Compare commits

...

26 Commits

  1. 4
      app/build.gradle
  2. 3
      app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt
  3. 4
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  4. 14
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  5. 2
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  6. 48
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  7. 8
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  8. 22
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReportManager.kt
  9. 12
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  10. 1
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  11. 1
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  12. 27
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  13. 2
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  14. 55
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  15. 9
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  16. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  17. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  18. 26
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  19. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  20. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  21. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  22. 9
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  23. 4
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  24. 91
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  25. 252
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  26. 27
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  27. 288
      app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt
  28. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  29. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt
  30. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  31. 14
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  32. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  33. 14
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  34. 412
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  35. 10
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  36. 6
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  37. 13
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  38. 9
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  39. 5
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt
  40. 23
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  41. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  42. 42
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  43. 5
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  44. 24
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt
  45. 36
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  46. 12
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDataListFragment.kt
  47. 21
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDataMultiSelectionFragment.kt
  48. 53
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  49. 9
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt
  50. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt
  51. 18
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt
  52. 28
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  53. 13
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt
  54. 3
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt
  55. 51
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt
  56. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/CustomFieldDataFragment.kt
  57. 55
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/DataManagerFragment.kt
  58. 21
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataFragment.kt
  59. 24
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt
  60. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListFragment.kt
  61. 41
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt
  62. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedHandHistoryRowRepresentableAdapter.kt
  63. 10
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedSessionRowRepresentableAdapter.kt
  64. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedTransactionRowRepresentableAdapter.kt
  65. 7
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt
  66. 42
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsFragment.kt
  67. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsViewModel.kt
  68. 3
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterHandler.kt
  69. 30
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterViewModel.kt
  70. 28
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersFragment.kt
  71. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListFragment.kt
  72. 16
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/editor/EditorFragment.kt
  73. 87
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/EditorViewModel.kt
  74. 571
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt
  75. 191
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt
  76. 10
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionViewModel.kt
  77. 26
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/DealtHandsPerHourFragment.kt
  78. 14
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/TransactionFilterFragment.kt
  79. 25
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  80. 8
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/StaticReport.kt
  81. 34
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/BottomSheetViewModel.kt
  82. 12
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/DataManagerViewModel.kt
  83. 4
      app/src/main/java/net/pokeranalytics/android/util/FakeDataManager.kt
  84. 6
      app/src/main/java/net/pokeranalytics/android/util/Global.kt
  85. 11
      app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt
  86. 12
      app/src/main/java/net/pokeranalytics/android/util/ListExtensions.kt
  87. 2
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  88. 18
      app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt
  89. 4
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  90. 18
      app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt
  91. 3
      app/src/main/res/layout/activity_home.xml
  92. 4
      app/src/main/res/layout/fragment_editable_data.xml

@ -37,8 +37,8 @@ android {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 32 targetSdkVersion 32
versionCode 146 versionCode 147
versionName "6.0.3" versionName "6.0.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

@ -39,8 +39,7 @@ class PerfsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
Seed(app).execute(realm) Seed(app).execute(realm)
realm.commitTransaction() realm.commitTransaction()
FakeDataManager.createFakeSessions(5000) { success ->
FakeDataManager.createFakeSessions(5000) {success ->
if (success) { if (success) {

@ -186,13 +186,13 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
Assert.fail("No std100 stat") Assert.fail("No std100 stat")
} }
results.computedStat(Stat.MAXIMUM_NETRESULT)?.let { results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let {
assertEquals(300.0, it.value, delta) assertEquals(300.0, it.value, delta)
} ?: run { } ?: run {
Assert.fail("No MAXIMUM_NETRESULT") Assert.fail("No MAXIMUM_NETRESULT")
} }
results.computedStat(Stat.MINIMUM_NETRESULT)?.let { results.computedStat(Stat.MINIMUM_NET_RESULT)?.let {
assertEquals(-100.0, it.value, delta) assertEquals(-100.0, it.value, delta)
} ?: run { } ?: run {
Assert.fail("No MINIMUM_NETRESULT") Assert.fail("No MINIMUM_NETRESULT")

@ -7,13 +7,15 @@ import com.google.firebase.FirebaseApp
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.FakeDataManager import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs import net.pokeranalytics.android.util.PokerAnalyticsLogs
@ -52,8 +54,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(14) .schemaVersion(15)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
.build() .build()
@ -82,15 +83,12 @@ class PokerAnalyticsApplication : Application() {
// Report // Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext) this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
SessionSetManager.configure()
// Infos // Infos
val locale = Locale.getDefault() val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}") CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
} }
/** /**
@ -103,7 +101,7 @@ class PokerAnalyticsApplication : Application() {
realm.close() realm.close()
if (sessionsCount < 10) { if (sessionsCount < 10) {
GlobalScope.launch { CoroutineScope(Dispatchers.Default).launch {
FakeDataManager.createFakeSessions(500) FakeDataManager.createFakeSessions(500)
} }
} }

@ -18,7 +18,7 @@ class CurrencyConverterApi {
companion object { companion object {
val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) { fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {

@ -9,13 +9,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveOnline import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.StaticReport import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.Global
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.CoroutineContext
interface NewPerformanceListener { interface NewPerformanceListener {
fun newBestPerformanceHandler() fun newBestPerformanceHandler()
@ -25,6 +25,7 @@ class ReportWhistleBlower(var context: Context) {
private var sessions: RealmResults<Session>? = null private var sessions: RealmResults<Session>? = null
private var results: RealmResults<Result>? = null private var results: RealmResults<Result>? = null
private var sessionSets: RealmResults<SessionSet>? = null
private var currentTask: ReportTask? = null private var currentTask: ReportTask? = null
@ -32,7 +33,7 @@ class ReportWhistleBlower(var context: Context) {
private val listeners: MutableList<NewPerformanceListener> = mutableListOf() private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
private var paused: Boolean = false private var paused: Boolean = true
private var timer: CountDownTimer? = null private var timer: CountDownTimer? = null
@ -50,6 +51,11 @@ class ReportWhistleBlower(var context: Context) {
requestReportLaunch() requestReportLaunch()
} }
this.sessionSets = realm.where(SessionSet::class.java).findAll()
this.sessionSets?.addChangeListener { _ ->
requestReportLaunch()
}
realm.close() realm.close()
} }
@ -62,7 +68,7 @@ class ReportWhistleBlower(var context: Context) {
} }
fun requestReportLaunch() { fun requestReportLaunch() {
Timber.d(">>> Launch report") // Timber.d(">>> Launch report")
if (paused) { if (paused) {
return return
@ -131,19 +137,20 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private var cancelled = false private var cancelled = false
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() { fun start() {
launchReports() launchReports()
} }
fun cancel() { fun cancel() {
Timber.d("Reportwhistleblower task CANCEL")
this.cancelled = true this.cancelled = true
} }
private fun launchReports() { private fun launchReports() {
CoroutineScope(coroutineContext).launch {
Timber.d("====== Report whistleblower launch batch...")
CoroutineScope(Dispatchers.Default).launch {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -157,7 +164,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
// CustomField // CustomField
val customFields = realm.where(CustomField::class.java) val customFields = realm.where(CustomField::class.java)
.equalTo("type", CustomField.Type.LIST.uniqueIdentifier).findAll() .equalTo("type", CustomField.Type.LIST.uniqueIdentifier)
.findAll()
for (customField in customFields) { for (customField in customFields) {
if (cancelled) { if (cancelled) {
break break
@ -171,7 +179,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private fun launchReport(realm: Realm, report: StaticReport) { private fun launchReport(realm: Realm, report: StaticReport) {
Timber.d(">>> launch report = $report") // Timber.d(">>> launch report = $report")
when (report) { when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, report) StaticReport.OptimalDuration -> launchOptimalDuration(realm, report)
@ -200,9 +208,9 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
val nameSeparator = " " val nameSeparator = " "
for (stat in result.options.stats) { for (stat in staticReport.performanceStats) {
Timber.d("analyse stat: $stat for report: $staticReport") // Timber.d("analyse stat: $stat for report: $staticReport")
// Get current performance // Get current performance
var query = performancesQuery(realm, staticReport, stat) var query = performancesQuery(realm, staticReport, stat)
@ -221,7 +229,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
val performanceQuery = computedResults.group.query val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator) val performanceName = performanceQuery.getName(this.context, nameSeparator)
Timber.d("Best computed = $performanceName, ${computedResults.computedStat(Stat.NET_RESULT)?.value}") val count = computedResults.computedStat(Stat.NUMBER_OF_GAMES)?.value?.toInt() ?: throw PAIllegalStateException("Number of games not found")
val notify = !(stat.hasEarlyVariance && count < 10)
var storePerf = true var storePerf = true
currentPerf?.let { currentPerf?.let {
@ -242,7 +251,10 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
currentPerf.objectId = performanceQuery.objectId currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id currentPerf.customFieldId = customField?.id
} }
this.whistleBlower.notify(currentPerf)
if (notify) {
this.whistleBlower.notify(currentPerf)
}
} }
} }
@ -257,14 +269,16 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
null null
) )
realm.executeTransaction { it.copyToRealm(performance) } realm.executeTransaction { it.copyToRealm(performance) }
this.whistleBlower.notify(performance) if (notify) {
this.whistleBlower.notify(performance)
}
} }
} ?: run { // if there is no max but a now irrelevant Performance, we delete it } ?: run { // if there is no max but a now irrelevant Performance, we delete it
Timber.d("NO best computed value, current perf = $currentPerf ") // Timber.d("NO best computed value, current perf = $currentPerf ")
currentPerf?.let { perf -> currentPerf?.let { perf ->
realm.executeTransaction { realm.executeTransaction {
Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}") // Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
perf.deleteFromRealm() perf.deleteFromRealm()
} }
} }

@ -319,6 +319,14 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
} }
} }
val hasEarlyVariance: Boolean
get() {
return when (this) {
HOURLY_RATE, AVERAGE, AVERAGE_NET_BB, AVERAGE_BUYIN, AVERAGE_HOURLY_DURATION -> true
else -> false
}
}
private val hasProgressValues: Boolean private val hasProgressValues: Boolean
get() { get() {
return when (this) { return when (this) {

@ -2,10 +2,7 @@ package net.pokeranalytics.android.calculus.bankroll
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
@ -15,9 +12,6 @@ import kotlin.coroutines.CoroutineContext
object BankrollReportManager { object BankrollReportManager {
private val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var reports: MutableMap<String?, BankrollReport> = mutableMapOf() private var reports: MutableMap<String?, BankrollReport> = mutableMapOf()
private var computableResults: RealmResults<ComputableResult> private var computableResults: RealmResults<ComputableResult>
@ -68,12 +62,12 @@ object BankrollReportManager {
} }
// otherwise compute it // otherwise compute it
GlobalScope.launch(coroutineContext) { CoroutineScope(Dispatchers.Main).launch {
var report: BankrollReport? = null var report: BankrollReport? = null
val coroutine = GlobalScope.async { val coroutine = CoroutineScope(Dispatchers.Default).async {
val s = Date() // val s = Date()
Timber.d(">>>>> start computing bankroll...") // Timber.d(">>>>> start computing bankroll...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -82,9 +76,9 @@ object BankrollReportManager {
realm.close() realm.close()
val e = Date() // val e = Date()
val duration = (e.time - s.time) / 1000.0 // val duration = (e.time - s.time) / 1000.0
Timber.d(">>>>> ended in $duration seconds") // Timber.d(">>>>> ended in $duration seconds")
} }
coroutine.await() coroutine.await()

@ -64,8 +64,8 @@ class CashGameOptimalDurationCalculator {
var end: Double? = null var end: Double? = null
var validBuckets = 0 var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() // val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
Timber.d("Stop notif > keys: $hkeys ") // Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) { for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0 val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) { if (start == null && sessionCount >= minimumValidityCount) {
@ -76,15 +76,15 @@ class CashGameOptimalDurationCalculator {
validBuckets++ validBuckets++
} }
} }
Timber.d("Stop notif > validBuckets: $validBuckets ") // Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) { if (!(start != null && end != null && (end - start) >= intervalValidity)) {
Timber.d("Stop notif > invalid setup: $start / $end ") // Timber.d("Stop notif > invalid setup: $start / $end ")
return null return null
} }
// define if we have enough sessions // define if we have enough sessions
if (sessions.size < 50) { if (sessions.size < 50) {
Timber.d("Stop notif > not enough sessions: ${sessions.size} ") // Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null return null
} }
@ -134,7 +134,7 @@ class CashGameOptimalDurationCalculator {
return bestDuration return bestDuration
} }
Timber.d("Stop notif > not found, best duration: $bestDuration") // Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close() realm.close()
return null return null
} }

@ -11,6 +11,7 @@ class ConfigurationException(message: String) : Exception(message)
class EnumIdentifierNotFoundException(message: String) : Exception(message) class EnumIdentifierNotFoundException(message: String) : Exception(message)
class MisconfiguredSavableEnumException(message: String) : Exception(message) class MisconfiguredSavableEnumException(message: String) : Exception(message)
class PAIllegalStateException(message: String) : Exception(message) class PAIllegalStateException(message: String) : Exception(message)
class PADataModelException(message: String) : Exception(message)
sealed class PokerAnalyticsException(message: String) : Exception(message) { sealed class PokerAnalyticsException(message: String) : Exception(message) {
object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition") object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition")

@ -68,7 +68,6 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach { realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
objects.add(QueryCondition.CustomFieldListQuery(it)) objects.add(QueryCondition.CustomFieldListQuery(it))
} }
objects.sort()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }

@ -8,7 +8,7 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.modules.data.*
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.Localizable import net.pokeranalytics.android.ui.view.Localizable
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
@ -30,7 +30,11 @@ enum class LiveData : Localizable {
PLAYER, PLAYER,
HAND_HISTORY; HAND_HISTORY;
var subType:Int? = null var subType: Int? = null
fun instanceFromOrdinal(ordinal: Int): LiveData {
return values()[ordinal]
}
val relatedEntity: Class<out Deletable> val relatedEntity: Class<out Deletable>
get() { get() {
@ -52,10 +56,10 @@ enum class LiveData : Localizable {
fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable { fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable {
val proxyItem: Deletable? = this.getData(realm, primaryKey) val proxyItem: Deletable? = this.getData(realm, primaryKey)
proxyItem?.let { return proxyItem?.let {
return realm.copyFromRealm(it) realm.copyFromRealm(it)
} ?: run { } ?: run {
return this.newEntity() this.newEntity()
} }
} }
@ -186,4 +190,17 @@ enum class LiveData : Localizable {
} }
} }
val dataFragment: EditableDataFragment
get() {
return when (this) {
BANKROLL -> BankrollDataFragment()
LOCATION -> LocationDataFragment()
TRANSACTION -> TransactionDataFragment()
CUSTOM_FIELD -> CustomFieldDataFragment()
TRANSACTION_TYPE -> TransactionTypeDataFragment()
PLAYER -> PlayerDataFragment()
else -> EditableDataFragment()
}
}
} }

@ -723,7 +723,7 @@ sealed class QueryCondition : RowRepresentable {
constructor(customFieldEntry: CustomFieldEntry) : this() { constructor(customFieldEntry: CustomFieldEntry) : this() {
this.setObject(customFieldEntry) this.setObject(customFieldEntry)
this.customFieldId = customFieldEntry.customField?.id this.customFieldId = customFieldEntry.managedCustomField?.id
?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue ?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} }

@ -68,10 +68,9 @@ class Patcher {
private fun patchMissingTransactionTypes(context: Context) { private fun patchMissingTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val transactionTypes = TransactionType.Value.values() realm.executeTransactionAsync { asyncRealm ->
val transactionTypes = TransactionType.Value.values()
realm.executeTransaction { Seed.createDefaultTransactionTypes(transactionTypes, context, asyncRealm)
Seed.createDefaultTransactionTypes(transactionTypes, context, realm)
} }
realm.close() realm.close()
@ -80,11 +79,12 @@ class Patcher {
private fun patchBreaks() { private fun patchBreaks() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val sets = realm.where(SessionSet::class.java).findAll() realm.executeTransactionAsync { asyncRealm ->
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsCash))
val results = realm.where(Result::class.java).findAll() val sets = asyncRealm.where(SessionSet::class.java).findAll()
val sessions = Filter.queryOn<Session>(asyncRealm, Query(QueryCondition.IsCash))
val results = asyncRealm.where(Result::class.java).findAll()
realm.executeTransaction {
sets.forEach { sets.forEach {
it.computeStats() it.computeStats()
} }
@ -98,13 +98,12 @@ class Patcher {
} }
realm.close() realm.close()
} }
private fun patchDefaultTransactionTypes(context: Context) { private fun patchDefaultTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
val tts = realm.where(TransactionType::class.java).findAll() val tts = asyncRealm.where(TransactionType::class.java).findAll()
tts.forEach { tt -> tts.forEach { tt ->
tt.kind?.let { kind -> tt.kind?.let { kind ->
val value = TransactionType.Value.values()[kind] val value = TransactionType.Value.values()[kind]
@ -117,8 +116,8 @@ class Patcher {
private fun patchStakes() { private fun patchStakes() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
val sessions = realm.where(Session::class.java).findAll() val sessions = asyncRealm.where(Session::class.java).findAll()
sessions.forEach { session -> sessions.forEach { session ->
val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull() val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) } val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
@ -128,7 +127,7 @@ class Patcher {
} }
} }
val handHistories = realm.where(HandHistory::class.java).findAll() val handHistories = asyncRealm.where(HandHistory::class.java).findAll()
handHistories.forEach { hh -> handHistories.forEach { hh ->
val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull() val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) } val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
@ -143,8 +142,8 @@ class Patcher {
private fun patchNegativeLimits() { private fun patchNegativeLimits() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
val sessions = realm.where(Session::class.java).lessThan("limit", 0).findAll() val sessions = asyncRealm.where(Session::class.java).lessThan("limit", 0).findAll()
sessions.forEach { session -> sessions.forEach { session ->
session.limit = null session.limit = null
} }
@ -154,10 +153,10 @@ class Patcher {
private fun cleanBlindsFilters() { private fun cleanBlindsFilters() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
val blindFilterConditions = realm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll() val blindFilterConditions = asyncRealm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll()
val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id } val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id }
val filters = realm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll() val filters = asyncRealm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll()
filters.deleteAllFromRealm() filters.deleteAllFromRealm()
} }
realm.close() realm.close()
@ -170,9 +169,9 @@ class Patcher {
private fun patchSessionSet() { private fun patchSessionSet() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
realm.where(SessionSet::class.java).findAll().deleteAllFromRealm() asyncRealm.where(SessionSet::class.java).findAll().deleteAllFromRealm()
val sessions = realm.where(Session::class.java).isNotNull("startDate").isNotNull("endDate").findAll() val sessions = asyncRealm.where(Session::class.java).isNotNull("startDate").isNotNull("endDate").findAll()
sessions.forEach { session -> sessions.forEach { session ->
SessionSetManager.updateTimeline(session) SessionSetManager.updateTimeline(session)
} }
@ -187,8 +186,8 @@ class Patcher {
*/ */
private fun patchComputableResults() { private fun patchComputableResults() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
val crs = realm.where(ComputableResult::class.java).findAll() val crs = asyncRealm.where(ComputableResult::class.java).findAll()
crs.forEach { cr -> crs.forEach { cr ->
cr.session?.let { cr.updateWith(it) } cr.session?.let { cr.updateWith(it) }
} }
@ -210,8 +209,8 @@ class Patcher {
private fun patchZeroTable() { private fun patchZeroTable() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val zero = 0 val zero = 0
val sessions = realm.where<Session>().equalTo("numberOfTables", zero).findAll() realm.executeTransactionAsync { asyncRealm ->
realm.executeTransaction { val sessions = asyncRealm.where<Session>().equalTo("numberOfTables", zero).findAll()
sessions.forEach { s -> sessions.forEach { s ->
s.numberOfTables = 1 s.numberOfTables = 1
} }
@ -221,8 +220,8 @@ class Patcher {
private fun patchRatedAmounts() { private fun patchRatedAmounts() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val transactions = realm.where<Transaction>().findAll() realm.executeTransactionAsync { asyncRealm ->
realm.executeTransaction { val transactions = asyncRealm.where<Transaction>().findAll()
transactions.forEach { t -> transactions.forEach { t ->
t.computeRatedAmount() t.computeRatedAmount()
} }

@ -335,6 +335,15 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++ currentVersion++
} }
// Migrate to version 15
if (currentVersion == 14) {
schema.get("Result")?.let { crs ->
crs.addField("id", String::class.java).setRequired("id", true)
crs.addPrimaryKey("id")
}
currentVersion++
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -42,7 +42,7 @@ open class Comment : RealmObject(), Manageable, RowRepresentable, RowUpdatable {
} }
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return if (content.isNotEmpty()) content else NULL_TEXT return content.ifEmpty { NULL_TEXT }
} }
// override fun startEditing(dataSource: Any?, parent: Fragment?) { // override fun startEditing(dataSource: Any?, parent: Fragment?) {

@ -22,7 +22,7 @@ open class ComputableResult : RealmObject(), Filterable {
var session: Session? = null var session: Session? = null
var ratedTips: Double = 0.0 private var ratedTips: Double = 0.0
fun updateWith(session: Session) { fun updateWith(session: Session) {
@ -38,8 +38,12 @@ open class ComputableResult : RealmObject(), Filterable {
this.bbNet = session.bbNet this.bbNet = session.bbNet
this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0 this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0
this.estimatedHands = session.estimatedHands this.estimatedHands = session.estimatedHands
this.bbPer100Hands =
session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 this.bbPer100Hands = if (this.estimatedHands > 0.0) {
session.bbNet / this.estimatedHands * 100
} else {
0.0
}
} }

@ -139,10 +139,6 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
} }
} }
override fun isValidForSave(): Boolean {
return super.isValidForSave()
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int { override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) { return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.cf_empty_field_error SaveValidityStatus.DATA_INVALID -> R.string.cf_empty_field_error
@ -250,7 +246,7 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
fun cleanupEntries() { // called when saving the custom field fun cleanupEntries() { // called when saving the custom field
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync {
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()
} }
@ -272,26 +268,6 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
} }
} }
/**
* Clean the entries if the type is not a list & remove the deleted entries from realm
*/
// fun cleanEntries(realm: Realm) {
// realm.executeTransaction {
//
// if (!isListType) {
// entriesToDelete.addAll(entries)
// entries.clear()
// }
//
// // @TODO
// entriesToDelete.forEach {
// Timber.d("Delete entry: V=${it.value} N=${it.numericValue} / ID=${it.id}")
// realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
// }
// entriesToDelete.clear()
// }
// }
/** /**
* Returns a comparison criteria based on this custom field * Returns a comparison criteria based on this custom field
*/ */

@ -22,6 +22,7 @@ import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.toCurrency import net.pokeranalytics.android.util.extensions.toCurrency
import timber.log.Timber
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.*
import java.util.Currency import java.util.Currency
@ -46,7 +47,7 @@ open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, R
@LinkingObjects("entries") @LinkingObjects("entries")
val customFields: RealmResults<CustomField>? = null val customFields: RealmResults<CustomField>? = null
val customField: CustomField? val managedCustomField: CustomField?
get() { get() {
return this.customFields?.first() return this.customFields?.first()
} }
@ -89,7 +90,7 @@ open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, R
} }
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return if (value.isNotEmpty()) value else NULL_TEXT return value.ifEmpty { NULL_TEXT }
} }
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? { override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
@ -136,8 +137,8 @@ open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, R
/** /**
* Return the amount * Return the amount
*/ */
fun getFormattedValue(currency: Currency? = null): String { fun getFormattedValue(parentCustomField: CustomField, currency: Currency? = null): String {
return when (customField?.type) { return when (parentCustomField.type) {
CustomField.Type.AMOUNT.uniqueIdentifier -> { CustomField.Type.AMOUNT.uniqueIdentifier -> {
numericValue?.toCurrency(currency) ?: run { NULL_TEXT } numericValue?.toCurrency(currency) ?: run { NULL_TEXT }
} }
@ -145,6 +146,7 @@ open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, R
NumberFormat.getInstance().format(this.numericValue) NumberFormat.getInstance().format(this.numericValue)
} }
else -> { else -> {
Timber.d("FORMATTED = $value")
value value
} }
} }

@ -77,7 +77,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
var filterConditions: RealmList<FilterCondition> = RealmList() var filterConditions: RealmList<FilterCondition> = RealmList()
private set private set
private var filterableTypeUniqueIdentifier: Int? = null var filterableTypeUniqueIdentifier: Int? = null
val filterableType: FilterableType val filterableType: FilterableType
get() { get() {
@ -109,7 +109,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
val previousCondition = filterConditions.filter { val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
} }
filterConditions.removeAll(previousCondition) filterConditions.removeAll(previousCondition.toSet())
filterConditions.add(newFilterCondition) filterConditions.add(newFilterCondition)
} }
} }
@ -118,7 +118,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
fun remove(filterCategoryRow: FilterCategoryRow) { fun remove(filterCategoryRow: FilterCategoryRow) {
val sections = filterCategoryRow.filterSectionRows.map { it.name } val sections = filterCategoryRow.filterSectionRows.map { it.name }
val savedSections = filterConditions.filter { sections.contains(it.sectionName) } val savedSections = filterConditions.filter { sections.contains(it.sectionName) }
this.filterConditions.removeAll(savedSections) this.filterConditions.removeAll(savedSections.toSet())
} }
fun countBy(filterCategoryRow: FilterCategoryRow): Int { fun countBy(filterCategoryRow: FilterCategoryRow): Int {

@ -63,16 +63,6 @@ open class FilterCondition() : RealmObject() {
} }
} }
fun <T> getv(clazz: Class<T>) : T {
return when (clazz) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
Date::class -> dateValue ?: Date()
String::class -> stringValue ?: ""
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} as T
}
inline fun <reified T> getValue(): T { inline fun <reified T> getValue(): T {
return when (T::class) { return when (T::class) {
Int::class -> intValue ?: 0 Int::class -> intValue ?: 0

@ -50,13 +50,6 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
@Ignore @Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java override val ownerClass: Class<out RealmModel> = Session::class.java
fun getNotNullShortName() : String {
this.shortName?.let {
return it
}
return this.name
}
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.name return this.name
} }
@ -71,7 +64,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT SimpleRow.NAME -> this.name.ifEmpty { NULL_TEXT }
GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0) else -> return super.charSequenceForRow(row, context, 0)
} }

@ -83,7 +83,7 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
PlayerPropertiesRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT PlayerPropertiesRow.NAME -> this.name.ifEmpty { NULL_TEXT }
else -> return super.charSequenceForRow(row, context, 0) else -> return super.charSequenceForRow(row, context, 0)
} }
} }
@ -174,7 +174,7 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
*/ */
fun cleanupComments() { // called when saving the custom field fun cleanupComments() { // called when saving the custom field
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync {
this.commentsToDelete.forEach { // entries are out of realm this.commentsToDelete.forEach { // entries are out of realm
realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm() realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
} }

@ -3,18 +3,20 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass import io.realm.annotations.RealmClass
import net.pokeranalytics.android.exceptions.PADataModelException
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 java.util.*
@RealmClass @RealmClass
open class Result : RealmObject(), Filterable { open class Result : RealmObject(), Filterable {
companion object { companion object {
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition>): String? { fun fieldNameForQueryType(queryCondition: Class <out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let { Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it" return "sessions.$it"
} }
@ -22,13 +24,17 @@ open class Result : RealmObject(), Filterable {
} }
} }
@PrimaryKey
var id = UUID.randomUUID().toString()
/** /**
* The buyin amount * The buyin amount
*/ */
var buyin: Double? = null var buyin: Double? = null
set(value) { set(value) {
field = value field = value
this.computeNumberOfRebuy() // this.computeNumberOfRebuy()
this.computeNet(true) this.computeNet(true)
} }
@ -39,9 +45,6 @@ open class Result : RealmObject(), Filterable {
set(value) { set(value) {
field = value field = value
this.computeNet(true) this.computeNet(true)
if (value != null) {
this.session?.end()
}
} }
/** /**
@ -49,20 +52,8 @@ open class Result : RealmObject(), Filterable {
*/ */
var netResult: Double? = null var netResult: Double? = null
set(value) { set(value) {
// this.session?.bankroll?.let { bankroll ->
// if (bankroll.live) {
// throw PAIllegalStateException("Can't set net result on a live bankroll")
// }
// } ?: run {
// throw PAIllegalStateException("Session doesn't have any bankroll")
// }
field = value field = value
this.computeNet(false) this.computeNet(false)
if (value != null) {
this.session?.end()
}
} }
/** /**
@ -75,10 +66,6 @@ open class Result : RealmObject(), Filterable {
* Tips * Tips
*/ */
var tips: Double? = null var tips: Double? = null
set(value) {
field = value
this.session?.computeStats()
}
// The transactions associated with the Result, impacting the result // The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList() var transactions: RealmList<Transaction> = RealmList()
@ -91,21 +78,23 @@ open class Result : RealmObject(), Filterable {
var tournamentFinalPosition: Int? = null var tournamentFinalPosition: Int? = null
// Number of rebuys // Number of rebuys
var numberOfRebuy: Double? = null private var numberOfRebuy: Double? = null
@LinkingObjects("result") @LinkingObjects("result")
private val sessions: RealmResults<Session>? = null private val sessions: RealmResults<Session>? = null
@Ignore private val managedSession: Session
val session: Session? = this.sessions?.firstOrNull() get() {
return this.sessions?.firstOrNull() ?: throw PADataModelException("Unmanaged Result")
}
/** /**
* Returns 1 if the session is positive * Returns 1 if the session is positive
*/ */
val isPositive: Int val isPositive: Int
get() { get() {
return if (session?.isTournament() == true) { return if (managedSession.isTournament()) {
if (this.cashout ?: -1.0 >= 0.0) 1 else 0 // if cashout is null we want to count a negative session if ((this.cashout ?: -1.0) >= 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else { } else {
if (this.net >= 0.0) 1 else 0 if (this.net >= 0.0) 1 else 0
} }
@ -124,11 +113,9 @@ open class Result : RealmObject(), Filterable {
} else if (buyin != null || cashout != null) { } else if (buyin != null || cashout != null) {
useBuyin = true useBuyin = true
} else { } else {
this.session?.let { session -> if (this.managedSession.isCashGame() && !this.managedSession.isLive) {
if (session.isCashGame() && !session.isLive) { useBuyin = false
useBuyin = false }
}
}
} }
} }
@ -142,31 +129,29 @@ open class Result : RealmObject(), Filterable {
} }
// Precompute results // Precompute results
this.session?.computeStats() // this.managedSession.computeStats()
this.session?.sessionSet?.computeStats() // this.managedSession.sessionSet?.computeStats()
} }
// Computes the number of rebuy // Computes the number of rebuy
fun computeNumberOfRebuy() { fun computeNumberOfRebuy() {
this.session?.let { if (this.managedSession.isCashGame()) {
if (it.isCashGame()) { this.managedSession.cgBiggestBet?.let { bb ->
it.cgBiggestBet?.let { bb -> if (bb > 0.0) {
if (bb > 0.0) { this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0) } else {
} else { this.numberOfRebuy = null
this.numberOfRebuy = null }
} }
} } else {
} else { this.managedSession.tournamentEntryFee?.let { entryFee ->
it.tournamentEntryFee?.let { entryFee -> if (entryFee > 0.0) {
if (entryFee > 0.0) { this.numberOfRebuy = (this.buyin ?: 0.0) / entryFee
this.numberOfRebuy = (this.buyin ?: 0.0) / entryFee } else {
} else { this.numberOfRebuy = null
this.numberOfRebuy = null }
} }
} }
}
}
} }
// @todo tips? // @todo tips?

@ -67,24 +67,32 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
companion object { companion object {
fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null, managed: Boolean = true): Session { fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session {
val session = Session() val session = Session()
session.result = Result() session.result = Result()
if (bankroll != null) { if (bankroll != null) {
session.bankroll = bankroll session.bankroll = realm.copyFromRealm(bankroll)
} else { } else {
session.bankroll = realm.where<Bankroll>().findFirst() realm.where<Bankroll>().findFirst()?.let { br ->
session.bankroll = realm.copyFromRealm(br)
}
} }
session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal
session.limit = Limit.NO.ordinal session.limit = Limit.NO.ordinal
session.game = realm.where(Game::class.java).equalTo("shortName", "HE").findFirst()
return if (managed) { realm.where(Game::class.java)
realm.copyToRealm(session) .equalTo("shortName", "HE").findFirst()?.let {
} else { session.game = realm.copyFromRealm(it)
session
} }
return session
// return if (managed) {
// realm.copyToRealm(session)
// } else {
// session
// }
} }
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? { fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
@ -157,6 +165,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
@Ignore @Ignore
val computableResult: ComputableResult? = this.computableResults?.firstOrNull() val computableResult: ComputableResult? = this.computableResults?.firstOrNull()
// Timed interface // Timed interface
override var dayOfWeek: Int? = null override var dayOfWeek: Int? = null
@ -204,12 +213,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.updateTimeParameter(field) this.updateTimeParameter(field)
this.computeNetDuration() this.computeNetDuration()
// nullifies enddate when setting the start date after the end date // nullifies the endDate when setting the start date after the end date
if (value != null && this.endDate != null && value.after(this.endDate)) { if (value != null && this.endDate != null && value.after(this.endDate)) {
this.endDate = null this.endDate = null
} }
this.dateChanged() this.dateChanged()
this.computeStats() // this.computeStats()
} }
/** /**
@ -230,7 +239,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.computeNetDuration() this.computeNetDuration()
this.dateChanged() this.dateChanged()
this.defineDefaultTournamentBuyinIfNecessary() this.defineDefaultTournamentBuyinIfNecessary()
this.computeStats() // this.computeStats()
} }
/** /**
@ -240,7 +249,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
field = value field = value
this.computeNetDuration() this.computeNetDuration()
this.computeStats() // this.computeStats()
} }
/** /**
@ -252,10 +261,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* The start date of the break * The start date of the break
*/ */
override var pauseDate: Date? = null override var pauseDate: Date? = null
set(value) {
field = value
// this.updateRowRepresentation()
}
// The session set containing the sessions, which can contain multiple endedSessions // The session set containing the sessions, which can contain multiple endedSessions
var sessionSet: SessionSet? = null var sessionSet: SessionSet? = null
@ -268,7 +273,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
field = value field = value
this.generateStakes() this.generateStakes()
this.computeStats() // this.computeStats()
// this.updateRowRepresentation() // this.updateRowRepresentation()
} }
@ -296,7 +301,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) { set(value) {
if (value > 0) { if (value > 0) {
field = value field = value
this.computeStats() // this.computeStats()
} }
} }
@ -314,16 +319,13 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The small blind value // The small blind value
var cgOldSmallBlind: Double? = null var cgOldSmallBlind: Double? = null
set(value) {
field = value
}
// The big blind value // The big blind value
var cgOldBigBlind: Double? = null var cgOldBigBlind: Double? = null
set(value) { set(value) {
field = value field = value
this.computeStats() this.computeStats()
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
// var blinds: String? = null // var blinds: String? = null
@ -334,8 +336,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
field = value field = value
this.generateStakes() this.generateStakes()
this.defineHighestBet() this.defineHighestBet()
this.computeStats() // this.computeStats()
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
var cgBlinds: String? = null var cgBlinds: String? = null
@ -343,8 +345,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
field = cleanupBlinds(value) field = cleanupBlinds(value)
this.generateStakes() this.generateStakes()
this.defineHighestBet() this.defineHighestBet()
this.computeStats() // this.computeStats()
this.result?.computeNumberOfRebuy() // this.result?.computeNumberOfRebuy()
} }
var cgBiggestBet: Double? = null var cgBiggestBet: Double? = null
@ -355,10 +357,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The entry fee of the tournament // The entry fee of the tournament
var tournamentEntryFee: Double? = null var tournamentEntryFee: Double? = null
set(value) {
field = value
this.result?.computeNumberOfRebuy()
}
// The total number of players who participated in the tournament // The total number of players who participated in the tournament
var tournamentNumberOfPlayers: Int? = null var tournamentNumberOfPlayers: Int? = null
@ -377,28 +375,29 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The number of hands played during the sessions // The number of hands played during the sessions
var handsCount: Int? = null var handsCount: Int? = null
set(value) {
field = value
this.computeStats()
}
fun bankrollHasBeenUpdated() { fun bankrollHasBeenUpdated() {
this.generateStakes() this.generateStakes()
} }
/**
* Manages impacts on SessionSets
* Should be called when the start / end date are changed
*/
private fun dateChanged() { private fun dateChanged() {
if (this.endDate != null) { SessionSetManager.sessionDateChanged(this)
SessionSetManager.updateTimeline(this)
} else if (this.sessionSet != null) {
SessionSetManager.removeFromTimeline(this)
}
// this.updateRowRepresentation()
} }
// /**
// * Manages impacts on SessionSets
// * Should be called when the start / end date are changed
// */
// private fun dateChanged() {
// SessionSetManager.updatedSession(this)
//// if (this.endDate != null) {
//// SessionSetManager.updateTimeline(this)
//// } else if (this.sessionSet != null) {
//// SessionSetManager.removeFromTimeline(this)
//// }
//// this.updateRowRepresentation()
// }
/** /**
* Returns a non-null date for the session * Returns a non-null date for the session
*/ */
@ -436,7 +435,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
get() { get() {
val bb = this.cgBiggestBet val bb = this.cgBiggestBet
val result = this.result val result = this.result
return if (bb != null && result != null) { return if (bb != null && bb > 0.0 && result != null) {
result.net / bb result.net / bb
} else { } else {
0.0 0.0
@ -471,6 +470,11 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
return this.result?.net ?: 0.0 return this.result?.net ?: 0.0
} }
fun preCompute() {
this.computeStats()
this.result?.computeNumberOfRebuy()
}
/** /**
* Pre-compute various statIds * Pre-compute various statIds
*/ */
@ -493,16 +497,21 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
} }
this.sessionSet?.computeStats() this.sessionSet?.computeStats()
} }
/** /**
* Approximates the number of hands played per hour at the table * Approximates the number of hands played per hour at the table
*/ */
val numberOfHandsPerHour: Double private val numberOfHandsPerHour: Double
get() { get() {
val tableSize = this.tableSize ?: 9 // 9 is the default table size if null val tableSize = this.tableSize ?: 9 // 9 is the default table size if null
val config = UserConfig.getConfiguration(this.realm)
val playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour var playerHandsPerHour = 0
UserConfig.getConfiguration(this.realm) { config ->
playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour
}
return this.numberOfTables * playerHandsPerHour / tableSize.toDouble() return this.numberOfTables * playerHandsPerHour / tableSize.toDouble()
} }
@ -557,7 +566,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* Start or continue a session * Start or continue a session
*/ */
fun startOrContinue() { fun startOrContinue() {
realm.executeTransaction { // realm.executeTransaction {
when (val state = getState()) { when (val state = getState()) {
SessionState.PENDING, SessionState.PLANNED -> { SessionState.PENDING, SessionState.PLANNED -> {
this.startDate = Date() this.startDate = Date()
@ -576,7 +585,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
throw PAIllegalStateException("unmanaged session state: $state") throw PAIllegalStateException("unmanaged session state: $state")
} }
} }
} // }
} }
private fun defineDefaultTournamentBuyinIfNecessary() { private fun defineDefaultTournamentBuyinIfNecessary() {
@ -589,28 +598,28 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* Pause a session * Pause a session
*/ */
fun pause() { fun pause() {
realm.executeTransaction { // realm.executeTransaction {
when (val state = getState()) { when (val state = getState()) {
SessionState.STARTED -> { SessionState.STARTED -> {
this.pauseDate = Date() this.pauseDate = Date()
} }
else -> throw PAIllegalStateException("Pausing a session in an unmanaged state: $state") else -> throw PAIllegalStateException("Pausing a session in an unmanaged state: $state")
} }
} // }
} }
/** /**
* Stop a session * Stop a session
*/ */
fun stop(context: Context) { fun stop(context: Context) {
realm.executeTransaction { // realm.executeTransaction {
when (val state = getState()) { when (val state = getState()) {
SessionState.STARTED, SessionState.PAUSED -> { SessionState.STARTED, SessionState.PAUSED -> {
this.end() this.end()
} }
else -> throw Exception("Stopping session in unmanaged state: $state") else -> throw Exception("Stopping session in unmanaged state: $state")
} }
} // }
cancelStopNotification(context) cancelStopNotification(context)
} }
@ -618,12 +627,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* Restart a session * Restart a session
*/ */
fun restart() { fun restart() {
realm.executeTransaction { // realm.executeTransaction {
this.pauseDate = null this.pauseDate = null
this.startDate = Date() this.startDate = Date()
this.endDate = null this.endDate = null
this.breakDuration = 0L this.breakDuration = 0L
} // }
} }
/** /**
@ -660,7 +669,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
/** /**
* Return the game titleResId * Return the game titleResId
* Example: NL Holdem * Example: NL Hold'em
*/ */
fun getFormattedGame(): String { fun getFormattedGame(): String {
var gameTitle = "" var gameTitle = ""
@ -672,46 +681,13 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
if (game != null) { if (game != null) {
gameTitle += game?.name gameTitle += game?.name
} }
return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT return gameTitle.ifBlank { NULL_TEXT }
} }
fun getFormattedStakes(): String { fun getFormattedStakes(): String {
return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT } return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT }
//
// val formattedBlinds = StakesHolder.formattedBlinds(this.cgBlinds, this.currency)
// val formattedAntes = StakesHolder.formattedAnte(this.cgAnte, this.currency)
//
// return StakesHolder.formattedStakes(formattedBlinds, formattedAntes)
//
//
// val components = arrayListOf<String>()
// this.formattedBlinds?.let { components.add(it) }
// this.formattedAnte?.let { components.add("($it)") }
//
// return if (components.isNotEmpty()) {
// components.joinToString(" ")
// } else {
// NULL_TEXT
// }
} }
// fun formatBlinds() {
// blinds = null
// if (cgBigBlind == null) return
// cgBigBlind?.let { bb ->
// val sb = cgSmallBlind ?: bb / 2.0
// val preFormattedBlinds = "${sb.formatted}/${bb.round()}"
// println("<<<<<< bb.toCurrency(currency) : ${bb.toCurrency(currency)}")
// println("<<<<<< preFormattedBlinds : $preFormattedBlinds")
// val regex = Regex("-?\\d+(\\.\\d+)?")
// blinds = bb.toCurrency(currency).replace(regex, preFormattedBlinds)
// println("<<<<<< blinds = $blinds")
// }
// }
// LifeCycle // LifeCycle
/** /**
@ -721,10 +697,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
CrashLogging.log("Deletes session. Id = ${this.id}") CrashLogging.log("Deletes session. Id = ${this.id}")
if (isValid) { if (isValid) {
realm.executeTransaction { // realm.executeTransaction {
cleanup() cleanup()
deleteFromRealm() deleteFromRealm()
} // }
} else { } else {
CrashLogging.log("Attempt to delete an invalid session") CrashLogging.log("Attempt to delete an invalid session")
} }
@ -741,7 +717,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
// cleanup unnecessary related objects // cleanup unnecessary related objects
this.result?.deleteFromRealm() this.result?.deleteFromRealm()
this.computableResults?.deleteAllFromRealm() this.computableResult?.deleteFromRealm()
} }
@ -776,32 +752,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
when (row) { when (row) {
SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll? SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
SessionPropertiesRow.STAKES -> if (value is Stakes) { SessionPropertiesRow.STAKES -> if (value is Stakes) {
if (value.ante != null) { if (value.ante != null) {
this.cgAnte = value.ante this.cgAnte = value.ante
} }
if (value.blinds != null) { if (value.blinds != null) {
this.cgBlinds = value.blinds this.cgBlinds = value.blinds
} }
// cgSmallBlind = try {
// (value[0] as String? ?: "0").toDouble()
// } catch (e: Exception) {
// null
// }
//
// cgBigBlind = try {
// (value[1] as String? ?: "0").toDouble()
// } catch (e: Exception) {
// null
// }
//
// cgBigBlind?.let {
// if (cgSmallBlind == null || cgSmallBlind == 0.0) {
// cgSmallBlind = it / 2.0
// }
// }
} else if (value == null) { } else if (value == null) {
this.cgBlinds = null this.cgBlinds = null
this.cgAnte = null this.cgAnte = null
@ -812,15 +768,20 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> { SessionPropertiesRow.BUY_IN -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.buyin = value as Double? localResult.buyin = value as Double?
// this.updateRowRepresentation()
} }
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> { SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.cashout = value as Double? localResult.cashout = value as Double?
if (value != null) {
this.end()
}
} }
SessionPropertiesRow.NET_RESULT -> { SessionPropertiesRow.NET_RESULT -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.netResult = value as Double? localResult.netResult = value as Double?
if (value != null) {
this.end()
}
} }
SessionPropertiesRow.COMMENT -> comment = value as String? ?: "" SessionPropertiesRow.COMMENT -> comment = value as String? ?: ""
SessionPropertiesRow.END_DATE -> if (value is Date?) { SessionPropertiesRow.END_DATE -> if (value is Date?) {
@ -876,20 +837,23 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName? SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName?
SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal
SessionPropertiesRow.TOURNAMENT_FEATURE -> { SessionPropertiesRow.TOURNAMENT_FEATURE -> {
this.tournamentFeatures.clear()
value?.let { value?.let {
tournamentFeatures = RealmList() tournamentFeatures = RealmList()
tournamentFeatures.addAll((it as ArrayList<TournamentFeature>)) tournamentFeatures.addAll((it as List<TournamentFeature>))
} ?: run {
tournamentFeatures.removeAll(this.tournamentFeatures)
} }
} }
SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt() SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt()
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables = (value as Double?)?.toInt() ?: 1 SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables = (value as Double?)?.toInt() ?: 1
is CustomField -> { is CustomField -> {
customFieldEntries.filter { it.customField?.id == row.id }.let {
customFieldEntries.removeAll(it) val entryIds = row.entries.map { it.id }
} val entries = this.customFieldEntries.intersectBy(entryIds) { it.id }
this.customFieldEntries.removeAll(entries)
// customFieldEntries.filter { it.customField?.id == row.id }.let {
// customFieldEntries.removeAll(it.toSet())
// }
when (row.type) { when (row.type) {
CustomField.Type.AMOUNT.uniqueIdentifier, CustomField.Type.AMOUNT.uniqueIdentifier,
CustomField.Type.NUMBER.uniqueIdentifier -> { CustomField.Type.NUMBER.uniqueIdentifier -> {
@ -914,10 +878,11 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
private fun getOrCreateResult(): Result { private fun getOrCreateResult(): Result {
return this.result return this.result
?: run { ?: run {
val result = realm.createObject(Result::class.java) val result = Result()
this.result = result // result.inverseSession = WeakReference(this)
result this.result = result
} result
}
} }
// Stat Entry // Stat Entry
@ -1028,7 +993,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT SessionPropertiesRow.COMMENT -> this.comment.ifEmpty { NULL_TEXT }
SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.GAME -> getFormattedGame() SessionPropertiesRow.GAME -> getFormattedGame()
SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
@ -1063,8 +1028,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context) SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context)
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString() SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString()
is CustomField -> { is CustomField -> {
customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry -> val entryIds = this.customFieldEntries.map { it.id }
return customFieldEntry.getFormattedValue(currency) val entries = row.entries.intersectBy(entryIds) { it.id }
entries.firstOrNull()?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(row, currency)
} }
return NULL_TEXT return NULL_TEXT
} }
@ -1086,33 +1053,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.result?.netResult = null this.result?.netResult = null
} }
/// Stakes
// fun generateStakes() {
//
// if (this.cgAnte == null && this.cgAnte == null) {
// this.cgStakes = null
// return
// }
//
// val components = arrayListOf<String>()
//
// this.cgBlinds?.let { components.add("${cbBlinds}${it}") }
// this.cgAnte?.let { components.add("${cbAnte}${it.formatted}") }
//
// val code = this.bankroll?.currency?.code ?: UserDefaults.currency.currencyCode
// components.add("${cbCode}${code}")
//
// this.cgStakes = components.joinToString(cbSeparator)
// }
//
// fun defineHighestBet() {
// val bets = arrayListOf<Double>()
// this.cgAnte?.let { bets.add(it) }
// bets.addAll(this.blindValues)
// this.cgBiggestBet = bets.maxOrNull()
// }
private fun cleanupBlinds(blinds: String?): String? { private fun cleanupBlinds(blinds: String?): String? {
if (blinds == null) { if (blinds == null) {

@ -15,11 +15,12 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.Timed import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import kotlin.math.min
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import kotlin.math.max
open class SessionSet : RealmObject(), Timed, Filterable {
open class SessionSet() : RealmObject(), Timed, Filterable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -64,7 +65,7 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0 this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0 this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0 this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L updateBreakDuration()
} }
/** /**
@ -75,7 +76,7 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
var ratedNet: Double = 0.0 var ratedNet: Double = 0.0
val hourlyRate: Double private val hourlyRate: Double
get() { get() {
return this.ratedNet / this.hourlyDuration return this.ratedNet / this.hourlyDuration
} }
@ -84,7 +85,7 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
var bbNet: BB = 0.0 var bbNet: BB = 0.0
val bbHourlyRate: BB private val bbHourlyRate: BB
get() { get() {
return this.bbNet / this.hourlyDuration return this.bbNet / this.hourlyDuration
} }
@ -142,5 +143,21 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = SessionSet::class.java override val realmObjectClass: Class<out Identifiable> = SessionSet::class.java
private fun updateBreakDuration() {
var longestNetDuration = 0L
var maxBreakDuration = 0L
this.sessions?.let { sessions ->
for (session in sessions) {
longestNetDuration = max(longestNetDuration, session.netDuration)
maxBreakDuration = max(session.breakDuration, maxBreakDuration)
}
}
val maxSetBreak = endDate.time - startDate.time - longestNetDuration
this.breakDuration = min(maxBreakDuration, maxSetBreak)
}
} }

@ -1,288 +0,0 @@
//package net.pokeranalytics.android.model.realm
//
//import io.realm.RealmObject
//import io.realm.RealmQuery
//import io.realm.RealmResults
//import io.realm.annotations.Ignore
//import io.realm.annotations.LinkingObjects
//import net.pokeranalytics.android.exceptions.ModelException
//import timber.log.Timber
//import java.util.*
//
//open class TimeFrame : RealmObject() {
//
// // A start date
// var startDate: Date = Date()
// private set(value) {
// field = value
// this.computeNetDuration()
// }
//
// // An end date
// var endDate: Date? = null
// private set(value) {
// field = value
// this.computeNetDuration()
// }
//
// // The latest pause date
// var pauseDate: Date? = null
// set(value) {
// field?.let {
// if (value == null && field != null) {
// breakDuration += Date().time - it.time
// }
// }
// field = value
// this.computeNetDuration()
// }
//
// // The break netDuration
// var breakDuration: Long = 0L
// set(value) {
// field = value
// this.computeNetDuration()
// }
//
// // the total netDuration
// var netDuration: Long = 0L
// private set
//
// var hourlyDuration: Double = 0.0
// get() {
// return this.netDuration / 3600000.0 // 3.6 millions of milliseconds
// }
//
// // Session
// @LinkingObjects("timeFrame")
// private val endedSessions: RealmResults<Session>? = null // we should have only one session
//
// @Ignore
// var session: Session? = null
// get() = if (this.endedSessions != null && this.endedSessions.isEmpty()) null else this.endedSessions?.first()
//
// // Group
// @LinkingObjects("timeFrame")
// private val sets: RealmResults<SessionSet>? = null // we should have only one sessionGroup
//
// @Ignore
// var set: SessionSet? = null
// get() = this.sets?.first()
//
// fun setStart(startDate: Date) {
// this.startDate = startDate
// this.session?.let {
// this.notifySessionDateChange(it)
// }
// }
//
// fun setEnd(endDate: Date?) {
// this.endDate = endDate
// this.session?.let {
// this.notifySessionDateChange(it)
// }
// }
//
// fun setDate(startDate: Date, endDate: Date?) {
// this.startDate = startDate
// this.endDate = endDate
//
// this.session?.let {
// this.notifySessionDateChange(it)
// }
// }
//
// /**
// * Computes the net netDuration of the session
// */
// private fun computeNetDuration() {
// var endDate: Date = this.endDate ?: Date()
// this.netDuration = endDate.time - this.startDate.time - this.breakDuration
// }
//
// /**
// * Queries all time frames that might be impacted by the date change
// * Makes all necessary changes to keep sequential time frames
// */
// fun notifySessionDateChange(owner: Session) {
//
// var query: RealmQuery<SessionSet> = this.realm.where(SessionSet::class.java)
// query.isNotNull("timeFrame")
//
//// Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}")
//
// val sets = realm.where(SessionSet::class.java).findAll()
//// Timber.d("set count = ${sets.size}")
//
// if (this.endDate == null) {
// query.greaterThanOrEqualTo("timeFrame.startDate", this.startDate)
// .or()
// .greaterThanOrEqualTo("timeFrame.endDate", this.startDate)
// .or()
// .isNull("timeFrame.endDate")
// } else {
// val endDate = this.endDate!!
// query
// .lessThanOrEqualTo("timeFrame.startDate", this.startDate)
// .greaterThanOrEqualTo("timeFrame.endDate", this.startDate)
// .or()
// .lessThanOrEqualTo("timeFrame.startDate", endDate)
// .greaterThanOrEqualTo("timeFrame.endDate", endDate)
// .or()
// .greaterThanOrEqualTo("timeFrame.startDate", this.startDate)
// .lessThanOrEqualTo("timeFrame.endDate", endDate)
// .or()
// .isNull("timeFrame.endDate")
// .lessThanOrEqualTo("timeFrame.startDate", endDate)
// }
//
// val sessionGroups = query.findAll()
//
// this.updateTimeFrames(sessionGroups, owner)
//
// }
//
// /**
// * Update Time frames from sets
// */
// private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, owner: Session) {
//
// when (sessionSets.size) {
// 0 -> this.createOrUpdateSessionSet(owner)
// 1 -> this.updateSessionGroup(owner, sessionSets.first()!!)
// else -> this.mergeSessionGroups(owner, sessionSets)
// }
//
// }
//
// /**
// * Creates the session sessionGroup when the session has none
// */
// private fun createOrUpdateSessionSet(owner: Session) {
//
// val set = owner.sessionSet
// if (set != null) {
// set.timeFrame?.startDate = this.startDate
// set.timeFrame?.endDate = this.endDate
// } else {
// this.createSessionSet(owner)
// }
//
//// Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")
// Timber.d("netDuration 1 = : ${set?.timeFrame?.netDuration}")
//
// }
//
// fun createSessionSet(owner: Session) {
// val set: SessionSet = SessionSet.newInstanceForResult(this.realm)
// set.timeFrame?.let {
// it.startDate = this.startDate
// it.endDate = this.endDate
// } ?: run {
// throw ModelException("TimeFrame should never be null here")
// }
//
// owner.sessionSet = set
// }
//
//
// /**
// * Single SessionSet update, the session might be the owner
// * Changes the sessionGroup timeframe using the current timeframe dates
// */
// private fun updateSessionGroup(owner: Session, sessionSet: SessionSet) {
//
// var timeFrame: TimeFrame = sessionSet.timeFrame!! // tested in the query
//// timeFrame.setDate(this.startDate, this.endDate)
//
// val sisterSessions = sessionSet.endedSessions!! // shouldn't crash ever
//
// // if we have only one session in the set and that it corresponds to the set
// if (sessionSet.endedSessions?.size == 1 && sessionSet.endedSessions?.first() == owner) {
// timeFrame.setDate(this.startDate, this.endDate)
// } else { // there are 2+ endedSessions to manage and possible splits
//
// val endDate = this.endDate
//
// // case where all endedSessions are over but the set is not, we might have a split, so we delete the set and save everything again
// if (endDate != null && sisterSessions.all { it.timeFrame?.endDate != null } && timeFrame.endDate == null) {
// var endedSessions = mutableListOf<Session>(owner)
// sessionSet.endedSessions?.forEach { endedSessions.add(it) }
// sessionSet.deleteFromRealm()
// endedSessions.forEach { it.timeFrame?.notifySessionDateChange(it) }
// } else {
//
// if (this.startDate.before(timeFrame.startDate)) {
// timeFrame.startDate = this.startDate
// }
// if (endDate != null && timeFrame.endDate != null && endDate.after(timeFrame.endDate)) {
// timeFrame.endDate = endDate
// } else if (endDate == null) {
// timeFrame.endDate = null
// }
//
// owner.sessionSet = sessionSet
//
//// Timber.d("sd = : ${sessionSet.timeFrame?.startDate}, ed = ${sessionSet.timeFrame?.endDate}")
// Timber.d("netDuration 2 = : ${sessionSet.timeFrame?.netDuration}")
// }
//
// }
//
// }
//
// /**
// * Multiple session sets update:
// * Merges all sets into one (delete all then create a new one)
// */
// private fun mergeSessionGroups(owner: Session, sessionSets: RealmResults<SessionSet>) {
//
// var startDate: Date = this.startDate
// var endDate: Date? = this.endDate
//
// // find earlier and later dates from all sets
// val timeFrames = sessionSets.mapNotNull { it.timeFrame }
// timeFrames.forEach { tf ->
// if (tf.startDate.before(startDate)) {
// startDate = tf.startDate
// }
//
// endDate?.let { ed ->
// tf.endDate?.let { tfed ->
// if (tfed.after(ed)) {
// endDate = tfed
// }
// }
// } ?: run {
// endDate = tf.endDate
// }
//
// }
//
// // get all endedSessions from sets
// var endedSessions = mutableSetOf<Session>()
// sessionSets.forEach { set ->
// set.endedSessions?.asIterable()?.let { endedSessions.addAll(it) }
// }
//
// // delete all sets
// sessionSets.deleteAllFromRealm()
//
// // Create a new sets
// val set: SessionSet = SessionSet.newInstanceForResult(this.realm)
// set.timeFrame?.let {
// it.setDate(startDate, endDate)
// } ?: run {
// throw ModelException("TimeFrame should never be null here")
// }
//
// // Add the session linked to this timeframe to the new sessionGroup
// owner.sessionSet = set
//
// // Add all orphan endedSessions
// endedSessions.forEach { it.sessionSet = set }
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
//
// }
//
//}

@ -62,7 +62,7 @@ open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, Na
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT SimpleRow.NAME -> this.name.ifEmpty { NULL_TEXT }
else -> return super.charSequenceForRow(row, context, 0) else -> return super.charSequenceForRow(row, context, 0)
} }
} }

@ -61,7 +61,7 @@ open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentabl
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT SimpleRow.NAME -> this.name.ifEmpty { NULL_TEXT }
else -> return super.charSequenceForRow(row, context,0) else -> return super.charSequenceForRow(row, context,0)
} }
} }

@ -79,7 +79,7 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
} }
// The amount of the transaction // The amount of the transaction
var ratedAmount: Double = 0.0 private var ratedAmount: Double = 0.0
// The date of the transaction // The date of the transaction
override var date: Date = Date() override var date: Date = Date()

@ -11,7 +11,19 @@ open class UserConfig : RealmObject() {
companion object { companion object {
fun getConfiguration(realm: Realm): UserConfig { fun getConfiguration(realm: Realm?, handler: (UserConfig) -> (Unit)) {
if (realm != null) {
handler(userConfiguration(realm))
} else {
val r = Realm.getDefaultInstance()
handler(userConfiguration(r))
r.close()
}
}
private fun userConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config -> realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config return config
} }

@ -313,13 +313,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
* Creates and affect a PlayerSetup at the given [positionIndex] * Creates and affect a PlayerSetup at the given [positionIndex]
*/ */
fun createPlayerSetup(positionIndex: Int): PlayerSetup { fun createPlayerSetup(positionIndex: Int): PlayerSetup {
val playerSetup = PlayerSetup()
val playerSetup = if (this.realm != null) {
this.realm.createObject(PlayerSetup::class.java) }
else {
PlayerSetup()
}
playerSetup.position = positionIndex playerSetup.position = positionIndex
this.playerSetups.add(playerSetup) this.playerSetups.add(playerSetup)
return playerSetup return playerSetup
@ -562,7 +556,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.winnerPots.clear() this.winnerPots.clear()
this.winnerPots.addAll(wonPots) this.winnerPots.addAll(wonPots)
Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map {it.position}} ") Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map { it.position }} ")
} }
/*** /***

@ -73,15 +73,21 @@ class FavoriteSessionFinder {
/** /**
* Copies the favorite session parameters on the [session] * Copies the favorite session parameters on the [session]
*/ */
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) { fun copyParametersFromFavoriteSession(realm: Realm, session: Session, location: Location?, context: Context) {
val favoriteSession = favoriteSession(session.type, location, session.realm, context) val favoriteSession = favoriteSession(session.type, location, realm, context)
favoriteSession?.let { fav -> favoriteSession?.let { fav ->
session.limit = fav.limit session.limit = fav.limit
session.game = fav.game
session.bankroll = fav.bankroll fav.game?.let {
session.game = realm.copyFromRealm(it)
}
fav.bankroll?.let {
session.bankroll = realm.copyFromRealm(it)
}
session.tableSize = fav.tableSize session.tableSize = fav.tableSize
when (session.type) { when (session.type) {

@ -1,209 +1,255 @@
package net.pokeranalytics.android.model.utils package net.pokeranalytics.android.model.utils
import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet import net.pokeranalytics.android.model.realm.SessionSet
import kotlin.math.max import net.pokeranalytics.android.util.Global
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
class CorruptSessionSetException(message: String) : Exception(message) class CorruptSessionSetException(message: String) : Exception(message)
/** /**
* The manager is in charge of updating the abstract concept of timeline, * The manager is in charge of updating the concept of timeline,
* representing the sequenced time frames where the user plays. * representing the time frames where the user plays.
*/ */
class SessionSetManager { object SessionSetManager {
companion object { var sessions: RealmResults<Session>
/** private val sessionIdsToProcess = mutableSetOf<String>()
* Updates the global timeline using the updated [session]
*/ fun configure() {} // launch init
fun updateTimeline(session: Session) {
fun sessionDateChanged(session: Session) {
if (!session.realm.isInTransaction) { this.sessionIdsToProcess.add(session.id)
throw PAIllegalStateException("realm should be in transaction at this point") }
}
init {
if (session.startDate == null) {
throw ModelException("Start date should never be null here") val realm = Realm.getDefaultInstance()
}
if (session.endDate == null) { this.sessions = realm.where(Session::class.java).findAllAsync()
throw ModelException("End date should never be null here") this.sessions.addChangeListener { _, _ ->
} if (Global.LAUNCH_ASYNC_LISTENERS) {
if (this.sessionIdsToProcess.isNotEmpty()) {
val sessionSets = this.matchingSets(session) realm.executeTransactionAsync { asyncRealm ->
cleanupSessionSets(session, sessionSets) processSessions(asyncRealm)
}
}
} }
}
private fun matchingSets(session: Session) : RealmResults<SessionSet> {
val realm = session.realm realm.close()
val endDate = session.endDate!! // tested above }
val startDate = session.startDate!!
private fun processSessions(realm: Realm) {
val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
Timber.d("***** processSessions, process count = ${sessionIdsToProcess.size}")
query
.lessThanOrEqualTo("startDate", startDate) for (sessionId in this.sessionIdsToProcess) {
.greaterThanOrEqualTo("endDate", startDate) realm.findById<Session>(sessionId)?.let { session ->
.or() if (session.startDate != null && session.endDate != null) {
.lessThanOrEqualTo("startDate", endDate) updateTimeline(session)
.greaterThanOrEqualTo("endDate", endDate) } else if (session.sessionSet != null) {
.or() removeFromTimeline(session)
.greaterThanOrEqualTo("startDate", startDate) }
.lessThanOrEqualTo("endDate", endDate) }
}
return query.findAll()
} this.sessionIdsToProcess.clear()
}
/**
* Multiple session sets update: /**
* Merges or splits session sets * Updates the global timeline using the updated [session]
* Does that by deleting then recreating */
*/ fun updateTimeline(session: Session) {
private fun cleanupSessionSets(session: Session, sessionSets: RealmResults<SessionSet>) {
// if (!session.realm.isInTransaction) {
// get all endedSessions from sets // throw PAIllegalStateException("realm should be in transaction at this point")
val allImpactedSessions = mutableSetOf<Session>() // }
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { allImpactedSessions.addAll(it) } if (session.startDate == null) {
} throw ModelException("Start date should never be null here")
allImpactedSessions.add(session) }
if (session.endDate == null) {
// delete all sets throw ModelException("End date should never be null here")
sessionSets.deleteAllFromRealm() }
allImpactedSessions.forEach { impactedSession -> val sessionSets = this.matchingSets(session)
val sets = matchingSets(impactedSession) cleanupSessionSets(session, sessionSets)
this.updateTimeFrames(sets, impactedSession)
} // val sessionId = session.id
// realm.executeTransactionAsync { asyncRealm ->
// asyncRealm.findById<Session>(sessionId)?.let { s ->
// val sessionSets = this.matchingSets(session)
// cleanupSessionSets(session, sessionSets)
// }
// }
}
private fun matchingSets(session: Session): RealmResults<SessionSet> {
val realm = session.realm
val endDate = session.endDate!! // tested above
val startDate = session.startDate!!
val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
query
.lessThanOrEqualTo("startDate", startDate)
.greaterThanOrEqualTo("endDate", startDate)
.or()
.lessThanOrEqualTo("startDate", endDate)
.greaterThanOrEqualTo("endDate", endDate)
.or()
.greaterThanOrEqualTo("startDate", startDate)
.lessThanOrEqualTo("endDate", endDate)
return query.findAll()
}
/**
* Multiple session sets update:
* Merges or splits session sets
* Does that by deleting then recreating
*/
private fun cleanupSessionSets(session: Session, sessionSets: RealmResults<SessionSet>) {
// get all endedSessions from sets
val allImpactedSessions = mutableSetOf<Session>()
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { allImpactedSessions.addAll(it) }
}
allImpactedSessions.add(session)
// delete all sets
sessionSets.deleteAllFromRealm()
allImpactedSessions.forEach { impactedSession ->
val sets = matchingSets(impactedSession)
this.updateTimeFrames(sets, impactedSession)
}
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}") // Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
} }
/**
/** * Update the global timeline using the impacted [sessionSets] and the updated [session]
* Update the global timeline using the impacted [sessionSets] and the updated [session] */
*/ private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, session: Session) {
private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, session: Session) { when (sessionSets.size) {
0 -> this.createOrUpdateSessionSet(session)
when (sessionSets.size) { else -> this.mergeSessionGroups(session, sessionSets)
0 -> this.createOrUpdateSessionSet(session) }
else -> this.mergeSessionGroups(session, sessionSets) }
}
/**
} * Creates or update the session set for the [session]
*/
/** private fun createOrUpdateSessionSet(session: Session) {
* Creates or update the session set for the [session]
*/ val set = session.sessionSet
private fun createOrUpdateSessionSet(session: Session) { if (set != null) {
set.startDate = session.startDate!! // tested above
val set = session.sessionSet set.endDate = session.endDate!!
if (set != null) { } else {
set.startDate = session.startDate!! // tested above this.createSessionSet(session)
set.endDate = session.endDate!! }
} else {
this.createSessionSet(session) }
}
/**
} * Create a set and affect it to the [session]
*/
/** private fun createSessionSet(session: Session) {
* Create a set and affect it to the [session] val set = SessionSet.newInstance(session.realm)
*/ set.startDate = session.startDate!!
private fun createSessionSet(session: Session) { set.endDate = session.endDate!!
val set: SessionSet = SessionSet.newInstance(session.realm) set.breakDuration = session.breakDuration
set.startDate = session.startDate!! session.sessionSet = set
set.endDate = session.endDate!! set.computeStats()
set.breakDuration = session.breakDuration }
session.sessionSet = set
set.computeStats() /**
} * Multiple session sets update:
* Merges all sets into one (delete all then create a new one)
/** */
* Multiple session sets update: private fun mergeSessionGroups(session: Session, sessionSets: RealmResults<SessionSet>) {
* Merges all sets into one (delete all then create a new one)
*/ var startDate = session.startDate!!
private fun mergeSessionGroups(session: Session, sessionSets: RealmResults<SessionSet>) { var endDate = session.endDate!!
var startDate = session.startDate!! // get all endedSessions from sets
var endDate = session.endDate!! val sessions = mutableSetOf<Session>()
sessionSets.forEach { set ->
// get all endedSessions from sets set.sessions?.asIterable()?.let { sessions.addAll(it) }
val sessions = mutableSetOf<Session>() }
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { sessions.addAll(it) } // find earlier and later dates from all sets
} sessions.forEach { s ->
// find earlier and later dates from all sets if (s.startDate != null && s.endDate != null) {
sessions.forEach { s -> val start = s.startDate!!
val end = s.endDate!!
if (s.startDate != null && s.endDate != null) { if (start.before(startDate)) {
val start = s.startDate!! startDate = start
val end = s.endDate!! }
if (start.before(startDate)) { if (end.after(endDate)) {
startDate = start endDate = end
} }
if (end.after(endDate)) { } else {
endDate = end throw CorruptSessionSetException("Set contains unfinished sessions!")
} }
} else { }
throw CorruptSessionSetException("Set contains unfinished sessions!")
} // delete all sets
} sessionSets.deleteAllFromRealm()
// delete all sets // Create a new set
sessionSets.deleteAllFromRealm() val set = SessionSet.newInstance(session.realm)
set.startDate = startDate
// Create a new set set.endDate = endDate
val set: SessionSet = SessionSet.newInstance(session.realm)
set.startDate = startDate // Add the session linked to this timeframe to the new sessionGroup
set.endDate = endDate session.sessionSet = set
// Add the session linked to this timeframe to the new sessionGroup // Add all orphan endedSessions
session.sessionSet = set sessions.forEach { s ->
s.sessionSet = set
// Add all orphan endedSessions }
sessions.forEach { s -> set.computeStats()
s.sessionSet = set
set.breakDuration = max(set.breakDuration, s.breakDuration)
}
set.computeStats()
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}") // Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
} }
/** /**
* Removes the [session] from the timeline * Removes the [session] from the timeline
*/ */
fun removeFromTimeline(session: Session) { fun removeFromTimeline(session: Session) {
if (!session.realm.isInTransaction) { // if (!session.realm.isInTransaction) {
throw PAIllegalStateException("realm should be in transaction at this point") // throw PAIllegalStateException("realm should be in transaction at this point")
} // }
val sessionSet = session.sessionSet val sessionSet = session.sessionSet
if (sessionSet != null) { if (sessionSet != null) {
val sessions = mutableSetOf<Session>() val sessions = mutableSetOf<Session>()
sessionSet.sessions?.asIterable()?.let { sessions.addAll(it) } sessionSet.sessions?.asIterable()?.let { sessions.addAll(it) }
sessions.remove(session) sessions.remove(session)
sessionSet.deleteFromRealm() sessionSet.deleteFromRealm()
sessions.forEach { sessions.forEach {
updateTimeline(it) updateTimeline(it)
} }
} }
} }
}
} }

@ -113,11 +113,11 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
val realm = getRealm() val realm = getRealm()
// observe currency changes // observe currency changes
this.currencies = realm.where(Currency::class.java).findAll() this.currencies = realm.where(Currency::class.java).findAllAsync()
this.currencies.addChangeListener { currencies, _ -> this.currencies.addChangeListener { _, _ ->
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
currencies.forEach { asyncRealm.where(Currency::class.java).findAll().forEach { currency ->
it.refreshRelatedRatedValues() currency.refreshRelatedRatedValues()
} }
} }
} }

@ -129,6 +129,12 @@ abstract class BaseActivity : AppCompatActivity() {
fragmentTransaction.commit() fragmentTransaction.commit()
} }
fun addFragmentWithBackStack(fragment: Fragment, containerId: Int) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(containerId, fragment)
fragmentTransaction.addToBackStack(fragment.javaClass.toString())
fragmentTransaction.commit()
}
/** /**
* Return the realm instance * Return the realm instance

@ -9,6 +9,7 @@ import android.provider.MediaStore
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -52,11 +53,11 @@ open class MediaActivity : BaseActivity() {
val filesList = ArrayList<File>() val filesList = ArrayList<File>()
GlobalScope.launch { CoroutineScope(Dispatchers.Default).launch {
if (tempFile != null) { if (tempFile != null) {
tempFile?.let { tempFile?.let {
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
filesList.add(it) filesList.add(it)
getPictures(filesList) getPictures(filesList)
} }
@ -65,7 +66,7 @@ open class MediaActivity : BaseActivity() {
data.clipData?.let { clipData -> data.clipData?.let { clipData ->
try { try {
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
isLoadingNewPictures() isLoadingNewPictures()
} }
@ -78,7 +79,7 @@ open class MediaActivity : BaseActivity() {
filesList.add(photoFile) filesList.add(photoFile)
} }
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
getPictures(filesList) getPictures(filesList)
} }
@ -90,7 +91,7 @@ open class MediaActivity : BaseActivity() {
data.data?.let { uri -> data.data?.let { uri ->
try { try {
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
isLoadingNewPictures() isLoadingNewPictures()
} }
@ -98,7 +99,7 @@ open class MediaActivity : BaseActivity() {
val photoFile = ImageUtils.createTempImageFile(this@MediaActivity) val photoFile = ImageUtils.createTempImageFile(this@MediaActivity)
ImageUtils.copyInputStreamToFile(inputStream!!, photoFile) ImageUtils.copyInputStreamToFile(inputStream!!, photoFile)
filesList.add(photoFile) filesList.add(photoFile)
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
getPictures(filesList) getPictures(filesList)
} }

@ -6,10 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentImportBinding import net.pokeranalytics.android.databinding.FragmentImportBinding
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
@ -89,9 +86,9 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.importer = CSVImporter(inputStream) this.importer = CSVImporter(inputStream)
this.importer.delegate = this this.importer.delegate = this
CoroutineScope(coroutineContext).launch { CoroutineScope(Dispatchers.Main).launch {
val coroutine = GlobalScope.async { val coroutine = CoroutineScope(Dispatchers.Default).async {
val s = Date() val s = Date()
Timber.d(">>> Start Import...") Timber.d(">>> Start Import...")

@ -118,11 +118,6 @@ class ReportCreationFragment : RealmFragment(), RowRepresentableDataSource, RowR
if (this.assistant.step == Assistant.Step.FINALIZE) { if (this.assistant.step == Assistant.Step.FINALIZE) {
// getRealm().executeTransaction {
// val rs = this.assistant.options.reportSetup("test")
// it.insert(rs)
// }
// launch report // launch report
this.finishActivityWithOptions(this.assistant.options, this.assistant.reportDisplay) this.finishActivityWithOptions(this.assistant.options, this.assistant.reportDisplay)

@ -11,13 +11,11 @@ import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -60,10 +58,7 @@ data class ReportSection(val report: StaticReport, var performances: MutableList
} }
data class PerformanceRow(val performance: Performance, val report: StaticReport): RowRepresentable { data class PerformanceRow(val performance: Performance, val report: StaticReport): RowRepresentable {
override val resId: Int? = this.performance.resId override val resId: Int? = this.performance.resId
override val viewType: Int = RowViewType.TITLE_BADGE_VALUE.identifier override val viewType: Int = RowViewType.TITLE_BADGE_VALUE.identifier
} }
@ -71,6 +66,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
private lateinit var reportSetups: RealmResults<ReportSetup> private lateinit var reportSetups: RealmResults<ReportSetup>
private lateinit var performances: RealmResults<Performance> private lateinit var performances: RealmResults<Performance>
private var adapterRows = mutableListOf<RowRepresentable>() private var adapterRows = mutableListOf<RowRepresentable>()
override fun deletableItems(): List<Deletable> { override fun deletableItems(): List<Deletable> {
@ -131,7 +127,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName)
itemToDeleteId?.let { id -> itemToDeleteId?.let { id ->
CoroutineScope(coroutineContext).launch { CoroutineScope(Dispatchers.Default).launch {
delay(300) delay(300)
deleteItem(dataListAdapter, reportSetups, id) deleteItem(dataListAdapter, reportSetups, id)
} }
@ -152,12 +148,12 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
* Init data * Init data
*/ */
private fun initData() { private fun initData() {
this.reportSetups = getRealm().where(ReportSetup::class.java).findAll().sort("name") this.reportSetups = getRealm().where(ReportSetup::class.java).findAllAsync().sort("name")
this.reportSetups.addChangeListener { _, _ -> this.reportSetups.addChangeListener { _, _ ->
this.updateRows() this.updateRows()
} }
this.performances = getRealm().where(Performance::class.java).findAll() this.performances = getRealm().where(Performance::class.java).findAllAsync()
this.performances.addChangeListener { _, _ -> this.performances.addChangeListener { _, _ ->
this.updateRows() this.updateRows()
} }
@ -313,17 +309,14 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
showLoader() showLoader()
CoroutineScope(coroutineContext).launch { CoroutineScope(Dispatchers.Default).launch {
val startDate = Date()
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh() realm.refresh()
val report = Calculator.computeStats(realm, options = options) val report = Calculator.computeStats(realm, options = options)
Timber.d("launchComputation: ${System.currentTimeMillis() - startDate.time}ms") CoroutineScope(Dispatchers.Main).launch {
launch(Dispatchers.Main) {
if (!isDetached) { if (!isDetached) {
hideLoader() hideLoader()
ReportActivity.newInstanceForResult(this@ReportsFragment, report, reportDisplay, reportName) ReportActivity.newInstanceForResult(this@ReportsFragment, report, reportDisplay, reportName)

@ -185,12 +185,12 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
Preferences.setCurrencyCode(currencyCode, requireContext()) Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransactionAsync { execRealm->
realm.where(Currency::class.java).findAll().forEach { currency -> execRealm.where(Currency::class.java).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate currency.rate = (currency.rate ?: 1.0) * rate
} }
realm.where(Session::class.java).findAll().forEach { session -> execRealm.where(Session::class.java).findAll().forEach { session ->
session.bankrollHasBeenUpdated() session.bankrollHasBeenUpdated()
} }
} }

@ -8,8 +8,9 @@ import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Dispatchers
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
@ -30,7 +31,6 @@ import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.modules.filter.FiltersActivity import net.pokeranalytics.android.ui.modules.filter.FiltersActivity
import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity
import timber.log.Timber import timber.log.Timber
import java.util.*
class StatisticsFragment : FilterableFragment(), RealmAsyncListener { class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
@ -76,6 +76,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
addRealmChangeListener(this, UserConfig::class.java) addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java) addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java) addRealmChangeListener(this, Transaction::class.java)
addRealmChangeListener(this, SessionSet::class.java)
} }
private fun initUI() { private fun initUI() {
@ -99,10 +100,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
private fun setTransactionFilterItemColor() { private fun setTransactionFilterItemColor() {
context?.let { context?.let {
val userConfig = UserConfig.getConfiguration(getRealm()) UserConfig.getConfiguration(getRealm()) { userConfig ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color)) this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
} }
} }
} }
@ -146,11 +148,13 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
// Business // Business
override fun asyncListenedEntityChange(realm: Realm) { override fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context. if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchStatComputation() launchStatComputation()
setTransactionFilterItemColor() setTransactionFilterItemColor()
} }
} }
/** /**
@ -158,11 +162,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/ */
private fun launchStatComputation() { private fun launchStatComputation() {
CoroutineScope(coroutineContext).launch { CoroutineScope(Dispatchers.Default).launch {
val async = GlobalScope.async { val async = async {
val s = Date() // val s = Date()
Timber.d(">>> start...") // Timber.d(">>> start...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh() realm.refresh()
@ -172,15 +176,17 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
realm.close() realm.close()
val e = Date() // val e = Date()
val duration = (e.time - s.time) / 1000.0 // val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds") // Timber.d(">>> ended in $duration seconds")
} }
async.await() async.await()
if (isAdded && !isDetached) { launch(Dispatchers.Main) {
tableReportFragment.showResults() if (isAdded && !isDetached) {
tableReportFragment.showResults()
}
} }
} }
} }
@ -247,7 +253,9 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
computedStats.addAll(tStats) computedStats.addAll(tStats)
options.stats = computedStats options.stats = computedStats
options.includedTransactions = UserConfig.getConfiguration(realm).transactionTypes(realm) UserConfig.getConfiguration(realm) { userConfig ->
options.includedTransactions = userConfig.transactionTypes(realm)
}
return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options) return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options)
} }

@ -109,6 +109,11 @@ abstract class BaseFragment : Fragment() {
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar -> view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
parentActivity?.setSupportActionBar(toolbar) parentActivity?.setSupportActionBar(toolbar)
} }
context?.getColor(R.color.kaki_darkest)?.let { color ->
view?.setBackgroundColor(color)
}
} }
/** /**

@ -8,15 +8,13 @@ import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.realm.RealmObject import io.realm.RealmObject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.util.extensions.findById
/** /**
* Deletable Item Fragment * Deletable Item Fragment
@ -56,7 +54,7 @@ abstract class DeletableItemFragment : RealmFragment() {
val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName)
itemToDeleteId?.let { id -> itemToDeleteId?.let { id ->
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
delay(300) delay(300)
deleteItem(dataListAdapter, deletableItems(), id) deleteItem(dataListAdapter, deletableItems(), id)
} }
@ -84,14 +82,21 @@ abstract class DeletableItemFragment : RealmFragment() {
if (itemToDelete is RealmObject && itemPosition != -1) { if (itemToDelete is RealmObject && itemPosition != -1) {
val itemClass = itemToDelete.realmObjectClass
// Check if the object is valid for the deletion // Check if the object is valid for the deletion
if (itemToDelete.isValidForDelete(this.getRealm())) { if (itemToDelete.isValidForDelete(this.getRealm())) {
deletedItem = getRealm().copyFromRealm(itemToDelete) deletedItem = getRealm().copyFromRealm(itemToDelete)
lastDeletedItemPosition = itemPosition lastDeletedItemPosition = itemPosition
getRealm().executeTransaction {
itemToDelete.deleteDependencies(it) getRealm().executeTransactionAsync { asyncRealm ->
itemToDelete.deleteFromRealm() val item = asyncRealm.findById(itemClass, itemId) as? Deletable
item?.let {
item.deleteDependencies(asyncRealm)
(item as RealmObject).deleteFromRealm()
}
} }
itemHasBeenReInserted = false itemHasBeenReInserted = false
updateUIAfterDeletion(itemId, itemPosition) updateUIAfterDeletion(itemId, itemPosition)
showUndoSnackBar() showUndoSnackBar()
@ -117,7 +122,8 @@ abstract class DeletableItemFragment : RealmFragment() {
snackBar?.setAction(R.string.cancel) { snackBar?.setAction(R.string.cancel) {
if (!itemHasBeenReInserted) { if (!itemHasBeenReInserted) {
itemHasBeenReInserted = true itemHasBeenReInserted = true
getRealm().executeTransaction { realm ->
getRealm().executeTransactionAsync { realm ->
deletedItem?.let { deletedItem?.let {
val item = realm.copyToRealmOrUpdate(it) val item = realm.copyToRealmOrUpdate(it)
updateUIAfterUndoDeletion(item) updateUIAfterUndoDeletion(item)

@ -7,20 +7,15 @@ import android.view.ViewGroup
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.Dispatchers
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber import net.pokeranalytics.android.util.Global
import kotlin.coroutines.CoroutineContext
interface RealmAsyncListener { interface RealmAsyncListener {
fun asyncListenedEntityChange(realm: Realm) fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>)
} }
open class RealmFragment : BaseFragment() { open class RealmFragment : BaseFragment() {
val coroutineContext: CoroutineContext
get() = Dispatchers.Main
/** /**
* A realm instance * A realm instance
*/ */
@ -41,15 +36,14 @@ open class RealmFragment : BaseFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
realm = Realm.getDefaultInstance() realm = Realm.getDefaultInstance()
this.observedEntities.forEach { // this.observedEntities.forEach {
val realmResults = realm.where(it).findAll() // val realmResults = realm.where(it).findAllAsync()
// realmResults.addChangeListener { t, _ ->
realmResults.addChangeListener { t, _ -> // this.entitiesChanged(it, t)
this.entitiesChanged(it, t) // }
} //
// this.observedRealmResults.add(realmResults)
this.observedRealmResults.add(realmResults) // }
}
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
@ -70,8 +64,10 @@ open class RealmFragment : BaseFragment() {
this.changeListener = listener this.changeListener = listener
val results = this.realm.where(clazz).findAllAsync() val results = this.realm.where(clazz).findAllAsync()
results.addChangeListener { t, _ -> results.addChangeListener { t, _ ->
Timber.d("Realm changes: ${realmResults?.size}, $this") // Timber.d("Realm changes: ${realmResults?.size}, $this")
this.changeListener?.asyncListenedEntityChange(t.realm) if (Global.LAUNCH_ASYNC_LISTENERS) {
this.changeListener?.asyncListenedEntityChange(t.realm, clazz)
}
} }
this.observedRealmResults.add(results) this.observedRealmResults.add(results)
} }
@ -90,11 +86,11 @@ open class RealmFragment : BaseFragment() {
/** /**
* A list of RealmModel classes to observe * A list of RealmModel classes to observe
*/ */
open val observedEntities: List<Class<out RealmModel>> = listOf() // open val observedEntities: List<Class<out RealmModel>> = listOf()
/** /**
* The method called when a change happened in any RealmResults * The method called when a change happened in any RealmResults
*/ */
open fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {} // open fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {}
} }

@ -13,7 +13,7 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
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
open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { open class BottomSheetDataListFragment : BottomSheetFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate {
private var _binding: BottomSheetListBinding? = null private var _binding: BottomSheetListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -62,18 +62,8 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.onRowSelected(position) this.onRowSelected(position)
dismiss() dismiss()
// this.viewModel.realmData?.let {
// val selectedData = it[position]
// selectedData?.let {data ->
// this.viewModel.onRowValueChanged(data)
//// this.delegate.onRowValueChanged(data, this.row)
// dismiss()
// }
// }
// super.onRowSelected(position, row, tag)
} }
/** /**

@ -2,22 +2,16 @@ package net.pokeranalytics.android.ui.fragment.components.bottomsheet
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.realm.RealmModel
import net.pokeranalytics.android.databinding.BottomSheetDoubleEditTextBinding
import net.pokeranalytics.android.databinding.BottomSheetListBinding
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
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
/** /**
* Manage multiple items selection in a bottom sheet list * Manage multiple items selection in a bottom sheet list
*/ */
open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() { open class BottomSheetDataMultiSelectionFragment : BottomSheetDataListFragment() {
override fun viewTypeForPosition(position: Int): Int { override fun viewTypeForPosition(position: Int): Int {
return RowViewType.TITLE_CHECK.ordinal return RowViewType.TITLE_CHECK.ordinal
@ -27,11 +21,14 @@ open class BottomSheetMultiSelectionFragment : BottomSheetListFragment() {
if (requestCode == REQUEST_CODE_ADD_NEW_OBJECT && resultCode == Activity.RESULT_OK && data != null) { if (requestCode == REQUEST_CODE_ADD_NEW_OBJECT && resultCode == Activity.RESULT_OK && data != null) {
val dataType = data.getIntExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, 0) val dataType = data.getIntExtra(EditableDataActivity.IntentKey.DATA_TYPE.keyName, 0)
val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName) val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName)
val pokerAnalyticsActivity = activity as BaseActivity
val liveDataType = LiveData.values()[dataType] val liveDataType = LiveData.values()[dataType]
val proxyItem: RealmModel? = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey) val realm = (activity as BaseActivity).getRealm()
this.model.selectedRows.add(proxyItem as RowRepresentable) liveDataType.getData(realm, primaryKey)?.let { proxyItem ->
this.refreshRow(proxyItem as RowRepresentable) val copy = realm.copyFromRealm(proxyItem)
this.model.selectedRows.add(copy as RowRepresentable)
this.refreshRow(copy as RowRepresentable)
}
// dataAdapter.refreshRow(proxyItem as RowRepresentable) // dataAdapter.refreshRow(proxyItem as RowRepresentable)
} }
} }

@ -18,13 +18,16 @@ import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.view.rows.TransactionPropertiesRow import net.pokeranalytics.android.ui.view.rows.TransactionPropertiesRow
import net.pokeranalytics.android.ui.viewmodel.AddedDataViewModel
import net.pokeranalytics.android.ui.viewmodel.BottomSheetViewModel import net.pokeranalytics.android.ui.viewmodel.BottomSheetViewModel
import net.pokeranalytics.android.ui.viewmodel.BottomSheetViewModelFactory import net.pokeranalytics.android.ui.viewmodel.BottomSheetViewModelFactory
import timber.log.Timber
import java.util.* import java.util.*
class BottomSheetConfig(var row: RowRepresentable, class BottomSheetConfig(var row: RowRepresentable,
@ -48,6 +51,10 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
private var _binding: FragmentBottomSheetBinding? = null private var _binding: FragmentBottomSheetBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
protected open val addedDataViewModel: AddedDataViewModel by lazy {
ViewModelProvider(requireActivity()).get(AddedDataViewModel::class.java)
}
companion object { companion object {
private var config: BottomSheetConfig? = null private var config: BottomSheetConfig? = null
@ -75,11 +82,11 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
private fun newInstance(bottomSheetType: BottomSheetType): BottomSheetFragment { private fun newInstance(bottomSheetType: BottomSheetType): BottomSheetFragment {
return when (bottomSheetType) { return when (bottomSheetType) {
BottomSheetType.NONE -> BottomSheetFragment() BottomSheetType.NONE -> BottomSheetFragment()
BottomSheetType.LIST -> BottomSheetListFragment() BottomSheetType.LIST -> BottomSheetDataListFragment()
BottomSheetType.LIST_STATIC -> BottomSheetStaticListFragment() BottomSheetType.LIST_STATIC -> BottomSheetStaticListFragment()
BottomSheetType.LIST_GAME -> BottomSheetListGameFragment() BottomSheetType.LIST_GAME -> BottomSheetListGameFragment()
BottomSheetType.DOUBLE_LIST -> BottomSheetListGameFragment() BottomSheetType.DOUBLE_LIST -> BottomSheetListGameFragment()
BottomSheetType.MULTI_SELECTION -> BottomSheetMultiSelectionFragment() BottomSheetType.MULTI_SELECTION -> BottomSheetDataMultiSelectionFragment()
BottomSheetType.GRID -> BottomSheetTableSizeGridFragment() BottomSheetType.GRID -> BottomSheetTableSizeGridFragment()
BottomSheetType.EDIT_TEXT -> BottomSheetEditTextFragment() BottomSheetType.EDIT_TEXT -> BottomSheetEditTextFragment()
BottomSheetType.EDIT_TEXT_MULTI_LINES -> BottomSheetEditTextMultiLinesFragment() BottomSheetType.EDIT_TEXT_MULTI_LINES -> BottomSheetEditTextMultiLinesFragment()
@ -95,6 +102,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//TODO: When dependency 'com.google.android.material:material:1.1.0' will be available in stable version, upgrade and remove that //TODO: When dependency 'com.google.android.material:material:1.1.0' will be available in stable version, upgrade and remove that
activity?.setTheme(R.style.PokerAnalyticsTheme) activity?.setTheme(R.style.PokerAnalyticsTheme)
_binding = FragmentBottomSheetBinding.inflate(inflater, container, false) _binding = FragmentBottomSheetBinding.inflate(inflater, container, false)
inflateContentView(inflater, binding.root) inflateContentView(inflater, binding.root)
return binding.root return binding.root
@ -169,10 +177,18 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName) val primaryKey = data.getStringExtra(EditableDataActivity.IntentKey.PRIMARY_KEY.keyName)
val pokerAnalyticsActivity = activity as BaseActivity val pokerAnalyticsActivity = activity as BaseActivity
val liveDataType = LiveData.values()[dataType] val liveDataType = LiveData.values()[dataType]
this.model.addedData = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey)
this.onRowValueChanged() val realm = pokerAnalyticsActivity.getRealm()
liveDataType.getData(realm, primaryKey)?.let {
this.model.addedData = realm.copyFromRealm(it)
this.onRowValueChanged()
dismiss()
} ?: run {
Timber.w("Data not found with primary key = $primaryKey")
}
// this.model.addedData = liveDataType.getData(pokerAnalyticsActivity.getRealm(), primaryKey)
// this.delegate.onRowValueChanged(proxyItem, this.row) // this.delegate.onRowValueChanged(proxyItem, this.row)
dismiss()
} }
} }
@ -224,11 +240,22 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
else -> throw PAIllegalStateException("row $it does not have an associated LiveData value") else -> throw PAIllegalStateException("row $it does not have an associated LiveData value")
} }
EditableDataActivity.newInstanceForResult( val fragment = liveData.dataFragment
this, //
liveData, this.addedDataViewModel.dataForAdd = true
requestCode = REQUEST_CODE_ADD_NEW_OBJECT
) val bundle = Bundle()
bundle.putInt(BaseFragment.BundleKey.DATA_TYPE.value, liveData.ordinal)
fragment.arguments = bundle
(this.activity as BaseActivity).addFragmentWithBackStack(fragment, R.id.container)
dismiss()
// EditableDataActivity.newInstanceForResult(
// this,
// liveData,
// requestCode = REQUEST_CODE_ADD_NEW_OBJECT
// )
true true
} }
@ -251,9 +278,9 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
return this.model.rowRepresentableEditDescriptors return this.model.rowRepresentableEditDescriptors
} }
private fun getValue(): Any? { // private fun getValue(): Any? {
return this.model.getValue() // return this.model.getValue()
} // }
private fun onClear() { private fun onClear() {
this.delegate?.onRowValueChanged(null, this.model.row) this.delegate?.onRowValueChanged(null, this.model.row)

@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.core.view.get import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import io.realm.RealmModel
import net.pokeranalytics.android.databinding.BottomSheetGameListBinding import net.pokeranalytics.android.databinding.BottomSheetGameListBinding
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
@ -17,7 +18,7 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
* Bottom Sheet List Game Fragment * Bottom Sheet List Game Fragment
* Display a list of game + chips to choose the game limit * Display a list of game + chips to choose the game limit
*/ */
class BottomSheetListGameFragment : BottomSheetListFragment() { class BottomSheetListGameFragment : BottomSheetDataListFragment() {
private var _binding: BottomSheetGameListBinding? = null private var _binding: BottomSheetGameListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -38,10 +39,10 @@ class BottomSheetListGameFragment : BottomSheetListFragment() {
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.model.realmData?.let { this.model.realmData?.let { realmResults ->
val selectedData = it[position] val selectedData = realmResults[position]
selectedData?.let { data -> selectedData?.let { data ->
this.model.someValues[1] = data this.model.someValues[1] = realmResults.realm.copyFromRealm(data as RealmModel)
this.onRowValueChanged() this.onRowValueChanged()
// this.delegate.onRowValueChanged(values, this.row) // this.delegate.onRowValueChanged(values, this.row)
dismiss() dismiss()

@ -45,9 +45,7 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.model.selectedRows.add(row) this.model.selectedRows.add(row)
this.onRowValueChanged() this.onRowValueChanged()
// this.delegate.onRowValueChanged(row, this.row)
dismiss() dismiss()
// super.onRowSelected(position, row, tag)
} }
/** /**

@ -121,9 +121,12 @@ abstract class AbstractReportFragment : DataManagerFragment() {
this.reportViewModel.title = name this.reportViewModel.title = name
val rs = this.model.item as ReportSetup val rs = this.model.item as ReportSetup
getRealm().executeTransaction { realm -> val setupId = rs.id
val firstSave = (this.model.primaryKey == null)
getRealm().executeTransactionAsync { realm ->
val firstSave = (this.model.primaryKey == null)
if (firstSave) { if (firstSave) {
val options = this.selectedReport.options val options = this.selectedReport.options
rs.name = name rs.name = name
@ -141,15 +144,18 @@ abstract class AbstractReportFragment : DataManagerFragment() {
options.filterId?.let { id -> options.filterId?.let { id ->
rs.filter = realm.findById(id) rs.filter = realm.findById(id)
} }
realm.copyToRealmOrUpdate(rs)
} else {
rs.name = name
realm.insertOrUpdate(rs) realm.insertOrUpdate(rs)
// realm.copyToRealmOrUpdate(rs)
} else {
realm.findById<ReportSetup>(setupId)?.let { reportSetup ->
reportSetup.name = name
realm.insertOrUpdate(reportSetup)
}
} }
} }
this.model.primaryKey = rs.id this.model.primaryKey = setupId
this.deleteButtonShouldAppear = true this.deleteButtonShouldAppear = true
setToolbarTitle(this.reportViewModel.title) setToolbarTitle(this.reportViewModel.title)
} }

@ -7,16 +7,13 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.Report import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentComposableTableReportBinding import net.pokeranalytics.android.databinding.FragmentComposableTableReportBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
@ -30,15 +27,10 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.StatRow import net.pokeranalytics.android.ui.view.rows.StatRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import timber.log.Timber
import java.util.*
open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, CoroutineScope, open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource,
RowRepresentableDelegate { RowRepresentableDelegate {
// override val coroutineContext: CoroutineContext
// get() = Dispatchers.Main
companion object { companion object {
/** /**
@ -211,12 +203,12 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
showLoader() showLoader()
GlobalScope.launch(coroutineContext) { CoroutineScope(Dispatchers.Default).launch {
var report: Report? = null var report: Report? = null
val test = GlobalScope.async { val test = async {
val s = Date() // val s = Date()
Timber.d(">>> start...") // Timber.d(">>> start...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh() realm.refresh()
@ -227,9 +219,9 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
realm.close() realm.close()
val e = Date() // val e = Date()
val duration = (e.time - s.time) / 1000.0 // val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds") // Timber.d(">>> ended in $duration seconds")
} }
test.await() test.await()

@ -11,6 +11,7 @@ import com.github.mikephil.charting.data.LineDataSet
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -169,10 +170,10 @@ class ProgressReportFragment : AbstractReportFragment() {
graphContainer.hideWithAnimation() graphContainer.hideWithAnimation()
progressBar.showWithAnimation() progressBar.showWithAnimation()
GlobalScope.launch { CoroutineScope(Dispatchers.Default).launch {
val s = Date() // val s = Date()
Timber.d(">>> start...") // Timber.d(">>> start...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -183,9 +184,9 @@ class ProgressReportFragment : AbstractReportFragment() {
realm.close() realm.close()
val e = Date() // val e = Date()
val duration = (e.time - s.time) / 1000.0 // val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds") // Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
setGraphData(report, aggregationType) setGraphData(report, aggregationType)

@ -13,6 +13,7 @@ import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.data.LineDataSet
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -167,7 +168,7 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource
this.model.computedResults?.let { computedResults -> this.model.computedResults?.let { computedResults ->
GlobalScope.launch { CoroutineScope(Dispatchers.Default).launch {
val startDate = Date() val startDate = Date()

@ -11,7 +11,6 @@ import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
@ -23,9 +22,7 @@ import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.UserConfig
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
@ -39,12 +36,11 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.set import kotlin.collections.set
class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentableDataSource, class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
RowRepresentableDelegate, RealmAsyncListener { RowRepresentableDelegate, RealmAsyncListener {
enum class TimeFilter { enum class TimeFilter {
@ -101,13 +97,15 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initData()
initUI() initUI()
initData()
addRealmChangeListener(this, UserConfig::class.java) addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java) addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java) addRealmChangeListener(this, Transaction::class.java)
addRealmChangeListener(this, SessionSet::class.java)
} }
private var transactionFilterMenuItem: MenuItem? = null private var transactionFilterMenuItem: MenuItem? = null
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -133,11 +131,10 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
private fun setTransactionFilterItemColor() { private fun setTransactionFilterItemColor() {
context?.let { context?.let {
val userConfig = UserConfig.getConfiguration(getRealm()) UserConfig.getConfiguration(getRealm()) { userConfig ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
this.transactionFilterMenuItem?.let { item -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color))
item.iconTintList = ColorStateList.valueOf(it.getColor(color))
} }
} }
} }
@ -211,9 +208,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
} }
} }
override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java)
// Business // Business
/** /**
@ -348,7 +342,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
binding.progressBar.showWithAnimation() binding.progressBar.showWithAnimation()
binding.recyclerView.hideWithAnimation() binding.recyclerView.hideWithAnimation()
GlobalScope.launch { CoroutineScope(Dispatchers.Default).launch {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh() realm.refresh()
@ -357,7 +351,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
realm.close() realm.close()
GlobalScope.launch(Dispatchers.Main) { launch(Dispatchers.Main) {
displayData() displayData()
} }
} }
@ -368,7 +362,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.time = Date().startOfMonth() calendar.time = Date().startOfMonth()
val startDate = Date() // val startDate = Date()
val requiredStats: List<Stat> = val requiredStats: List<Stat> =
listOf( listOf(
@ -378,7 +372,12 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Stat.STANDARD_DEVIATION_HOURLY Stat.STANDARD_DEVIATION_HOURLY
) )
val transactionTypes = UserConfig.getConfiguration(realm).transactionTypes(realm) var transactionTypes = listOf<TransactionType>()
UserConfig.getConfiguration(realm) { userConfig ->
transactionTypes = userConfig.transactionTypes(realm)
}
// val transactionTypes = UserConfig.getConfiguration(realm).transactionTypes(realm)
// All // All
val allOptions = Calculator.Options( val allOptions = Calculator.Options(
@ -490,7 +489,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
sortedMonthlyReports = monthlyReports.toSortedMap(compareByDescending { it }) sortedMonthlyReports = monthlyReports.toSortedMap(compareByDescending { it })
sortedYearlyReports = yearlyReports.toSortedMap(compareByDescending { it }) sortedYearlyReports = yearlyReports.toSortedMap(compareByDescending { it })
Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms") // Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms")
} }
@ -498,11 +497,11 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
* Display data * Display data
*/ */
private fun displayData() { private fun displayData() {
Timber.d("displayData") // Timber.d("displayData")
if (context == null) { return } // required because of launchAsyncStatComputation if (context == null) { return } // required because of launchAsyncStatComputation
val startDate = Date() // val startDate = Date()
datesForRows.clear() datesForRows.clear()
rows.clear() rows.clear()
@ -594,8 +593,8 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
} }
} }
Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms") // Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
Timber.d("Rows: ${rows.size}") // Timber.d("Rows: ${rows.size}")
this.calendarAdapter.notifyDataSetChanged() this.calendarAdapter.notifyDataSetChanged()
@ -604,11 +603,13 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
} }
override fun asyncListenedEntityChange(realm: Realm) { override fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context. if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchAsyncStatComputation() launchAsyncStatComputation()
setTransactionFilterItemColor() setTransactionFilterItemColor()
} }
} }
private fun showGridCalendar() { private fun showGridCalendar() {

@ -133,7 +133,7 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> if (customField.name.isNotEmpty()) customField.name else NULL_TEXT SimpleRow.NAME -> customField.name.ifEmpty { NULL_TEXT }
else -> super.charSequenceForRow(row, context, 0) else -> super.charSequenceForRow(row, context, 0)
} }
} }

@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -15,6 +14,7 @@ import net.pokeranalytics.android.model.interfaces.Savable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.viewmodel.AddedDataViewModel
import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel
open class DataManagerFragment : RealmFragment() { open class DataManagerFragment : RealmFragment() {
@ -23,12 +23,11 @@ open class DataManagerFragment : RealmFragment() {
ViewModelProvider(this).get(modelClass) ViewModelProvider(this).get(modelClass)
} }
open val modelClass: Class<out DataManagerViewModel> = DataManagerViewModel::class.java protected open val addedDataViewModel: AddedDataViewModel by lazy {
ViewModelProvider(requireActivity()).get(AddedDataViewModel::class.java)
}
// lateinit var item: Deletable open val modelClass: Class<out DataManagerViewModel> = DataManagerViewModel::class.java
// protected lateinit var liveDataType: LiveData
// protected var primaryKey: String? = null
// protected var dataType: Int? = null
var deleteButtonShouldAppear = false var deleteButtonShouldAppear = false
set(value) { set(value) {
@ -53,10 +52,22 @@ open class DataManagerFragment : RealmFragment() {
menu.clear() menu.clear()
inflater.inflate(R.menu.toolbar_editable_data, menu) inflater.inflate(R.menu.toolbar_editable_data, menu)
this.editableMenu = menu this.editableMenu = menu
setMenuListeners()
updateMenuUI() updateMenuUI()
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
} }
private fun setMenuListeners() {
editableMenu?.findItem(R.id.delete)?.setOnMenuItemClickListener {
deleteData()
return@setOnMenuItemClickListener true
}
editableMenu?.findItem(R.id.save)?.setOnMenuItemClickListener {
saveData()
return@setOnMenuItemClickListener true
}
}
/** /**
* Update menu UI * Update menu UI
*/ */
@ -65,13 +76,13 @@ open class DataManagerFragment : RealmFragment() {
editableMenu?.findItem(R.id.save)?.isVisible = this.saveButtonShouldAppear editableMenu?.findItem(R.id.save)?.isVisible = this.saveButtonShouldAppear
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { // override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { // when (item.itemId) {
R.id.save -> saveData() // R.id.save -> saveData()
R.id.delete -> deleteData() // R.id.delete -> deleteData()
} // }
return true // return true
} // }
/** /**
* Init data * Init data
@ -96,14 +107,20 @@ open class DataManagerFragment : RealmFragment() {
val status = savable.getSaveValidityStatus(realm = this.getRealm()) val status = savable.getSaveValidityStatus(realm = this.getRealm())
when (status) { when (status) {
SaveValidityStatus.VALID -> { SaveValidityStatus.VALID -> {
this.getRealm().executeTransaction {
val managedItem = it.copyToRealmOrUpdate(this.model.item) this.getRealm().executeTransactionAsync { asyncRealm ->
if (managedItem is Savable) { asyncRealm.insertOrUpdate(savable)
val uniqueIdentifier = managedItem.id
finishActivityWithResult(uniqueIdentifier)
}
} }
onDataSaved() onDataSaved()
if (this.addedDataViewModel.dataForAdd) {
this.addedDataViewModel.data.value = savable
this.parentActivity?.supportFragmentManager?.popBackStack()
} else {
val uniqueIdentifier = savable.id
finishActivityWithResult(uniqueIdentifier)
}
} }
else -> { else -> {
val message = savable.getFailedSaveMessage(status) val message = savable.getFailedSaveMessage(status)

@ -18,7 +18,6 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.RowUpdatable
open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegate { open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegate {
lateinit var rowRepresentableAdapter: RowRepresentableAdapter lateinit var rowRepresentableAdapter: RowRepresentableAdapter
@ -43,7 +42,6 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat
this.model.primaryKey = this.arguments?.getString(BundleKey.PRIMARY_KEY.value) this.model.primaryKey = this.arguments?.getString(BundleKey.PRIMARY_KEY.value)
this.model.loadItemWithRealm(getRealm()) this.model.loadItemWithRealm(getRealm())
} }
open fun indexOfFirstRowToSelect(): Int { open fun indexOfFirstRowToSelect(): Int {
@ -67,15 +65,20 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat
} }
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
getRealm().executeTransaction {
try {
(this.model.item as RowUpdatable).updateValue(value, row)
} catch (e: Exception) {
CrashLogging.log("Exception caught: row = $row, value=$value, class=${this.javaClass}")
throw e
}
(this.model.item as RowUpdatable).updateValue(value, row)
if (this.model.primaryKey != null) {
getRealm().executeTransactionAsync { asyncRealm ->
try {
asyncRealm.insertOrUpdate(this.model.item)
} catch (e: Exception) {
CrashLogging.log("Exception caught: row = $row, value=$value, class=${this.javaClass}")
throw e
}
}
} }
rowRepresentableAdapter.refreshRow(row) rowRepresentableAdapter.refreshRow(row)
} }

@ -2,8 +2,8 @@ package net.pokeranalytics.android.ui.modules.data
import android.content.Context import android.content.Context
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager
@ -33,6 +33,11 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
return this.model.item as Transaction return this.model.item as Transaction
} }
override fun onDestroyView() {
super.onDestroyView()
this.addedDataViewModel.data.removeObservers(this)
}
override fun initData() { override fun initData() {
super.initData() super.initData()
@ -40,9 +45,19 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
if (this.transaction.bankroll == null) { if (this.transaction.bankroll == null) {
val bankrolls = getRealm().where<Bankroll>().findAll() val bankrolls = getRealm().where<Bankroll>().findAll()
if (bankrolls.size == 1) { if (bankrolls.size == 1) {
this.transaction.bankroll = bankrolls.first() bankrolls.first()?.let { br ->
this.transaction.bankroll = getRealm().copyFromRealm(br)
}
} }
} }
this.addedDataViewModel.data.observeForever {
if (this.addedDataViewModel.dataForAdd) {
this.addedDataViewModel.dataForAdd = false
this.onRowValueChanged(it, this.addedDataViewModel.dataIdentifier)
}
}
} }
override fun indexOfFirstRowToSelect(): Int { override fun indexOfFirstRowToSelect(): Int {
@ -102,6 +117,9 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.addedDataViewModel.dataIdentifier = row
when (row) { when (row) {
TransactionPropertiesRow.DATE -> DateTimePickerManager.create( TransactionPropertiesRow.DATE -> DateTimePickerManager.create(
requireContext(), requireContext(),
@ -141,7 +159,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
when (val next = rows[index + 1]) { when (val next = rows[index + 1]) {
TransactionPropertiesRow.DATE, TransactionPropertiesRow.COMMENT -> {} TransactionPropertiesRow.DATE, TransactionPropertiesRow.COMMENT -> {}
else -> { else -> {
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
delay(200) delay(200)
onRowSelected(0, next) onRowSelected(0, next)
} }

@ -81,10 +81,14 @@ open class DataListFragment : DeletableItemFragment(), RowRepresentableDelegate
getRealm().where(this.model.identifiableClass) getRealm().where(this.model.identifiableClass)
.`in`("id", itemIds) .`in`("id", itemIds)
.sort(this.model.dataType.sortFields, this.model.dataType.sortOrders) .sort(this.model.dataType.sortFields, this.model.dataType.sortOrders)
.findAll() .findAllAsync()
} else { } else {
this.retrieveItems(getRealm()) this.retrieveItems(getRealm())
} }
items.addChangeListener { _, _ ->
this.dataListAdapter.notifyDataSetChanged()
}
this.model.setItemsList(items) this.model.setItemsList(items)
} }

@ -12,6 +12,7 @@ import com.android.billingclient.api.Purchase
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.BadgeUtils
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
@ -30,6 +31,7 @@ import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.extensions.openUrl import net.pokeranalytics.android.ui.extensions.openUrl
import net.pokeranalytics.android.ui.fragment.components.FilterableFragment import net.pokeranalytics.android.ui.fragment.components.FilterableFragment
import net.pokeranalytics.android.ui.fragment.components.RealmAsyncListener
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode
@ -44,9 +46,10 @@ import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.extensions.count import net.pokeranalytics.android.util.extensions.count
import net.pokeranalytics.android.util.extensions.findById
import java.util.* import java.util.*
class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseListener { class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseListener, RealmAsyncListener {
private enum class Tab { private enum class Tab {
SESSIONS, SESSIONS,
@ -83,14 +86,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
private var badgeDrawable: BadgeDrawable? = null private var badgeDrawable: BadgeDrawable? = null
override val observedEntities: List<Class<out RealmModel>> = override fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>) {
listOf(Session::class.java, Transaction::class.java, HandHistory::class.java)
override fun entitiesChanged(
clazz: Class<out RealmModel>,
results: RealmResults<out RealmModel>
) {
super.entitiesChanged(clazz, results)
when (clazz.kotlin) { when (clazz.kotlin) {
Session::class -> { Session::class -> {
@ -106,7 +102,6 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
this.handHistoryAdapter.notifyDataSetChanged() this.handHistoryAdapter.notifyDataSetChanged()
} }
} }
} }
private var _binding: FragmentFeedBinding? = null private var _binding: FragmentFeedBinding? = null
@ -141,11 +136,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
when (item.itemId) { when (item.itemId) {
R.id.duplicate -> { R.id.duplicate -> {
val session = this.sessionAdapter.sessionForPosition(menuPosition) val session = this.sessionAdapter.sessionForPosition(menuPosition)
if (session != null) { createNewSession(true, sessionId = session.id, duplicate = true)
createNewSession(true, sessionId = session.id, duplicate = true)
} else {
throw PAIllegalStateException("Session not found for duplicate at position: $menuPosition")
}
} }
} }
@ -157,6 +148,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
AppGuard.registerListener(this) AppGuard.registerListener(this)
addRealmChangeListener(this, Session::class.java)
addRealmChangeListener(this, Transaction::class.java)
addRealmChangeListener(this, HandHistory::class.java)
initUI() initUI()
initData() initData()
} }
@ -481,9 +476,8 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
// gets the first session of the adapter - the last created - to preconfigure the HH // gets the first session of the adapter - the last created - to preconfigure the HH
if (this.sessionAdapter.itemCount > 0) { if (this.sessionAdapter.itemCount > 0) {
this.sessionAdapter.sessionForPosition(0)?.let { session -> val session = this.sessionAdapter.sessionForPosition(0)
HandHistoryActivity.newInstance(this, session, false) HandHistoryActivity.newInstance(this, session, false)
} ?: throw PAIllegalStateException("Cannot happen")
} else { } else {
HandHistoryActivity.newInstance(this) HandHistoryActivity.newInstance(this)
} }
@ -493,9 +487,16 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
* Delete selected transaction * Delete selected transaction
*/ */
private fun deleteSelectedTransaction() { private fun deleteSelectedTransaction() {
getRealm().executeTransaction {
selectedTransaction?.deleteFromRealm() selectedTransaction?.id?.let { id ->
getRealm().executeTransactionAsync { asyncRealm ->
asyncRealm.findById<Transaction>(id)?.deleteFromRealm()
}
} }
// getRealm().executeTransaction {
// selectedTransaction?.deleteFromRealm()
// }
selectedTransactionPosition = -1 selectedTransactionPosition = -1
} }

@ -137,8 +137,6 @@ class FeedHandHistoryRowRepresentableAdapter(
this.sortedHeaders = this.headersPositions.toSortedMap() this.sortedHeaders = this.headersPositions.toSortedMap()
// Timber.d("]]] this.sortedHeaders = ${this.sortedHeaders}")
} }
/** /**

@ -100,7 +100,7 @@ class FeedSessionRowRepresentableAdapter(
BindableHolder { BindableHolder {
fun bind(title: String) { fun bind(title: String) {
// Title // Title
itemView.findViewById<AppCompatTextView>(net.pokeranalytics.android.R.id.title)?.let { itemView.findViewById<AppCompatTextView>(R.id.title)?.let {
it.text = title it.text = title
} }
} }
@ -144,7 +144,7 @@ class FeedSessionRowRepresentableAdapter(
// If the header has no date, it's a pending session // If the header has no date, it's a pending session
return if (sortedHeaders[position] == null) { return if (sortedHeaders[position] == null) {
context.getString(net.pokeranalytics.android.R.string.pending) context.getString(R.string.pending)
} else { } else {
// Else, return the formatted date // Else, return the formatted date
sortedHeaders[position]?.getMonthAndYear() ?: throw PAIllegalStateException("Null date should not happen there") sortedHeaders[position]?.getMonthAndYear() ?: throw PAIllegalStateException("Null date should not happen there")
@ -153,14 +153,14 @@ class FeedSessionRowRepresentableAdapter(
throw PAIllegalStateException("Any position should always have a header, position = $position") throw PAIllegalStateException("Any position should always have a header, position = $position")
} }
fun sessionForPosition(position: Int): Session? { fun sessionForPosition(position: Int): Session {
return this.getSessionForPosition(position) return this.getSessionForPosition(position)
} }
/** /**
* Get real index * Get real index
*/ */
private fun getSessionForPosition(position: Int): Session? { private fun getSessionForPosition(position: Int): Session {
// Row position // Row position
var headersBefore = 0 var headersBefore = 0
@ -182,7 +182,7 @@ class FeedSessionRowRepresentableAdapter(
allSessions.clear() allSessions.clear()
allSessions.addAll(this.pendingSessions) allSessions.addAll(this.pendingSessions)
allSessions.addAll(this.startedSessions) allSessions.addAll(this.startedSessions)
Timber.d("Update session list, total count = ${allSessions.size}") // Timber.d("Update session list, total count = ${allSessions.size}")
val headersPositions = HashMap<Int, Date?>() val headersPositions = HashMap<Int, Date?>()

@ -160,8 +160,6 @@ class FeedTransactionRowRepresentableAdapter(
*/ */
private fun checkHeaderCondition(currentCalendar: Calendar, previousYear: Int, previousMonth: Int): Boolean { private fun checkHeaderCondition(currentCalendar: Calendar, previousYear: Int, previousMonth: Int): Boolean {
return currentCalendar.get(Calendar.YEAR) == previousYear && currentCalendar.get(Calendar.MONTH) < previousMonth || (currentCalendar.get(Calendar.YEAR) < previousYear) return currentCalendar.get(Calendar.YEAR) == previousYear && currentCalendar.get(Calendar.MONTH) < previousMonth || (currentCalendar.get(Calendar.YEAR) < previousYear)
} }
} }

@ -9,10 +9,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewAnimationUtils import android.view.ViewAnimationUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.pokeranalytics.android.databinding.ActivityNewDataBinding import net.pokeranalytics.android.databinding.ActivityNewDataBinding
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.extensions.px import net.pokeranalytics.android.ui.extensions.px
@ -105,7 +102,7 @@ class NewDataMenuActivity : BaseActivity() {
val intent = Intent() val intent = Intent()
intent.putExtra(IntentKey.CHOICE.keyName, choice) intent.putExtra(IntentKey.CHOICE.keyName, choice)
setResult(RESULT_OK, intent) setResult(RESULT_OK, intent)
GlobalScope.launch(Dispatchers.Main) { CoroutineScope(Dispatchers.Main).launch {
delay(200) delay(200)
hideMenu() hideMenu()
} }

@ -90,7 +90,7 @@ open class FilterDetailsFragment : RealmFragment(), RowRepresentableDelegate {
this.arguments?.let { bundle -> this.arguments?.let { bundle ->
val filter = this.activityModel.currentFilter ?: throw PAIllegalStateException("Filter is null") val filter = this.activityModel.currentFilter
val category = bundle.getInt(BundleKey.DATA_TYPE.value) val category = bundle.getInt(BundleKey.DATA_TYPE.value)
val categoryRow = FilterCategoryRow.values()[category] val categoryRow = FilterCategoryRow.values()[category]
Timber.d("Category row = $categoryRow") Timber.d("Category row = $categoryRow")
@ -100,7 +100,7 @@ open class FilterDetailsFragment : RealmFragment(), RowRepresentableDelegate {
} ?: throw PAIllegalStateException("Missing bundle") } ?: throw PAIllegalStateException("Missing bundle")
Timber.d(">> Filter = ${this.activityModel.currentFilter}") // Timber.d(">> Filter = ${this.activityModel.currentFilter}")
Timber.d("selectedRow = ${this.activityModel.selectedCategoryRow}") Timber.d("selectedRow = ${this.activityModel.selectedCategoryRow}")
this.binding.toolbar.title = this.activityModel.selectedCategoryRow?.localizedTitle(requireContext()) this.binding.toolbar.title = this.activityModel.selectedCategoryRow?.localizedTitle(requireContext())
@ -230,26 +230,32 @@ open class FilterDetailsFragment : RealmFragment(), RowRepresentableDelegate {
*/ */
private fun saveData() { private fun saveData() {
val currentFilter = this.activityModel.currentFilter val filter = this.activityModel.currentFilter
//TODO: Save currentFilter details data // this.activityModel.currentFilter?.id?.let { filterId ->
Timber.d("Save data for queryWith: ${currentFilter?.id}")
this.model.selectedRows.forEach {
Timber.d("Selected rows: $it")
}
this.activityModel.selectedCategoryRow?.let { category -> this.activityModel.selectedCategoryRow?.let { category ->
getRealm().executeTransaction { // getRealm().executeTransactionAsync { asyncRealm ->
currentFilter?.remove(category) // asyncRealm.findById<Filter>(filterId)?.let { filter ->
val validConditions = this.model.selectedRows.filter { it.queryCondition != null } filter.remove(category)
currentFilter?.createOrUpdateFilterConditions(validConditions) val validConditions = this.model.selectedRows.filter { it.queryCondition != null }
} filter.createOrUpdateFilterConditions(validConditions)
} }
// }
// }
currentFilter?.filterConditions?.forEach { // }
Timber.d("Condition: $it")
} // //TODO: Save currentFilter details data
// Timber.d("Save data for queryWith: ${currentFilter?.id}")
// this.model.selectedRows.forEach {
// Timber.d("Selected rows: $it")
// }
// currentFilter?.filterConditions?.forEach {
// Timber.d("Condition: $it")
// }
// finishActivityWithResult(currentFilter?.id) // finishActivityWithResult(currentFilter?.id)
} }

@ -31,7 +31,7 @@ class FilterDetailsViewModel(categoryRow: FilterCategoryRow, var filter: Filter)
this.defineSelectedItems() this.defineSelectedItems()
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable> {
return this.rows return this.rows
} }

@ -45,9 +45,8 @@ interface FilterHandler {
fun saveFilter(context: Context, filterId: String) { fun saveFilter(context: Context, filterId: String) {
Preferences.setActiveFilterId(filterId, context) Preferences.setActiveFilterId(filterId, context)
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { executeRealm -> realm.executeTransactionAsync { executeRealm ->
currentFilter(context, executeRealm)?.let { currentFilter(context, executeRealm)?.let {
it.useCount++ it.useCount++
} }

@ -12,7 +12,7 @@ import net.pokeranalytics.android.util.extensions.findById
class FilterViewModel : ViewModel(), StaticRowRepresentableDataSource { class FilterViewModel : ViewModel(), StaticRowRepresentableDataSource {
var currentFilter: Filter? = null var currentFilter: Filter = Filter()
// Main // Main
var filterableType: FilterableType? = null var filterableType: FilterableType? = null
@ -27,27 +27,27 @@ class FilterViewModel : ViewModel(), StaticRowRepresentableDataSource {
fun init(realm: Realm) { fun init(realm: Realm) {
if (this.currentFilter != null) { // can be called twice and we don't want that // if (this.currentFilter != null) { // can be called twice and we don't want that
return // return
} // }
this.primaryKey?.let { this.primaryKey?.let {
val filter = realm.findById<Filter>(it) ?: throw PAIllegalStateException("Can't find filter with id=$it") val filter = realm.findById<Filter>(it) ?: throw PAIllegalStateException("Can't find filter with id=$it")
this.currentFilter = realm.copyFromRealm(filter) this.currentFilter = realm.copyFromRealm(filter)
this.isUpdating = true this.isUpdating = true
} ?: run { } ?: run {
this.filterableType?.uniqueIdentifier?.let { this.currentFilter.filterableTypeUniqueIdentifier = this.filterableType?.uniqueIdentifier
this.currentFilter = Filter.newInstance(it) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal)) // this.filterableType?.uniqueIdentifier?.let {
} // this.currentFilter = Filter.newInstance(it) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal))
// }
} }
// Create a copy if the user cancels the updates // Create a copy if the user cancels the updates
this.currentFilter?.let { // this.currentFilter?.let {
if (it.isValid && it.isManaged) { // if (it.isValid && it.isManaged) {
this.filterCopy = realm.copyFromRealm(it) // this.filterCopy = realm.copyFromRealm(it)
} // }
} // }
this.categoryRows.clear() this.categoryRows.clear()
@ -59,13 +59,13 @@ class FilterViewModel : ViewModel(), StaticRowRepresentableDataSource {
// Data source // Data source
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable> {
return this.categoryRows return this.categoryRows
} }
override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence { override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence {
// Return the number of selected filters for this category // Return the number of selected filters for this category
val count = this.currentFilter?.countBy(row as FilterCategoryRow) ?: 0 val count = this.currentFilter.countBy(row as FilterCategoryRow)
return if (count > 0) count.toString() else "" return if (count > 0) count.toString() else ""
} }

@ -15,7 +15,6 @@ import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
@ -105,7 +104,7 @@ open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.save -> validateUpdates() R.id.save -> save()
} }
return true return true
} }
@ -156,9 +155,7 @@ open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
val categoryRow = row as FilterCategoryRow val categoryRow = row as FilterCategoryRow
this.model.selectedCategoryRow = categoryRow this.model.selectedCategoryRow = categoryRow
this.model.currentFilter?.let { _ -> (activity as FiltersActivity).showDetailsFragment(categoryRow)
(activity as FiltersActivity).showDetailsFragment(categoryRow)
}
} }
/** /**
@ -217,18 +214,15 @@ open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
/** /**
* Validate the updates of the queryWith * Validate the updates of the queryWith
*/ */
private fun validateUpdates() { private fun save() {
val currentFilter = this.model.currentFilter val filter = this.model.currentFilter
getRealm().executeTransaction { realm -> getRealm().executeTransactionAsync { asyncRealm ->
currentFilter?.let { filter.name = filter.query.getName(requireContext())
it.name = it.query.getName(requireContext()) asyncRealm.insertOrUpdate(filter)
realm.copyToRealmOrUpdate(it) }
}
}
val filterId = currentFilter?.id ?: "" finishActivityWithResult(filter.id)
finishActivityWithResult(filterId)
} }
/** /**
@ -239,9 +233,9 @@ open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
val filterCopy = this.model.filterCopy val filterCopy = this.model.filterCopy
val filterId = filterCopy?.id ?: "" val filterId = filterCopy?.id ?: ""
getRealm().executeTransaction { realm -> getRealm().executeTransactionAsync { realm ->
filterCopy?.let { filterCopy?.let {
realm.copyToRealmOrUpdate(it) realm.insertOrUpdate(it)
} }
} }
finishActivityWithResult(filterId) finishActivityWithResult(filterId)

@ -15,8 +15,10 @@ open class FiltersListFragment : DataListFragment() {
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
is Filter -> { is Filter -> {
getRealm().executeTransaction {
row.updateValue(value, row) row.updateValue(value, row)
getRealm().executeTransactionAsync { asyncRealm ->
asyncRealm.insertOrUpdate(row)
} }
val index = this.model.items.indexOf(row) val index = this.model.items.indexOf(row)
this.dataListAdapter.notifyItemChanged(index) this.dataListAdapter.notifyItemChanged(index)

@ -202,12 +202,15 @@ class EditorFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListen
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
val playerId = data?.getStringExtra(BaseFragment.BundleKey.PRIMARY_KEY.value) val playerId = data?.getStringExtra(BaseFragment.BundleKey.PRIMARY_KEY.value)
?: throw PAIllegalStateException("Primary key not set where as activity has finished") ?: throw PAIllegalStateException("Primary key not set where as activity has finished")
getRealm().findById<Player>(playerId)?.let { player ->
this.model.playerSelected(player) val realm = getRealm()
realm.findById<Player>(playerId)?.let { player ->
val p = realm.copyFromRealm(player)
this.model.playerSelected(p)
} ?: throw PAIllegalStateException("Player (id=$playerId) not found") } ?: throw PAIllegalStateException("Player (id=$playerId) not found")
this.editorAdapter.notifyDataSetChanged() this.editorAdapter.notifyDataSetChanged()
} }
} }
} }
@ -684,10 +687,9 @@ class EditorFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListen
*/ */
private fun deleteHand() { private fun deleteHand() {
getRealm().findById<HandHistory>(this.model.handHistory.id)?.let { hh -> val hhId = this.model.handHistory.id
getRealm().executeTransaction { getRealm().executeTransactionAsync { asyncRealm ->
hh.deleteFromRealm() asyncRealm.findById<HandHistory>(hhId)?.deleteFromRealm()
}
} }
this.activity?.finish() this.activity?.finish()

@ -5,10 +5,7 @@ import android.text.InputType
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.HandSetup import net.pokeranalytics.android.model.handhistory.HandSetup
@ -27,12 +24,10 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.formatted import net.pokeranalytics.android.util.extensions.formatted
import timber.log.Timber import timber.log.Timber
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.ParseException import java.text.ParseException
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass import kotlin.reflect.KClass
enum class HHKeyboard { enum class HHKeyboard {
@ -49,9 +44,6 @@ interface PlayerSetupCreationListener {
class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer, ActionListListener { class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer, ActionListListener {
private val coroutineContext: CoroutineContext
get() = Dispatchers.Main
/*** /***
* The hand history * The hand history
*/ */
@ -641,33 +633,30 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
* Saves the current hand state in the database * Saves the current hand state in the database
*/ */
fun save(realm: Realm) { fun save(realm: Realm) {
realm.executeTransaction { this.handHistory.actions.clear()
this.handHistory.actions.clear() val actions = this.sortedActions.map { it.action }
val actions = this.sortedActions.map { it.action } this.handHistory.actions.addAll(actions)
this.handHistory.actions.addAll(actions)
if (!this.handHistory.isManaged) { realm.executeTransactionAsync { asyncRealm ->
realm.copyToRealmOrUpdate(this.handHistory) this.handHistory.defineWinnerPositions()
} asyncRealm.insertOrUpdate(this.handHistory)
} }
this.defineWinnerPositions()
} }
private fun defineWinnerPositions() { // private fun defineWinnerPositions() {
//
val hhId = this.handHistory.id // val hhId = this.handHistory.id
GlobalScope.launch(coroutineContext) { // CoroutineScope(Dispatchers.Default).launch {
val c = GlobalScope.async { // val realm = Realm.getDefaultInstance()
val realm = Realm.getDefaultInstance() //
realm.executeTransaction { // realm.findById<HandHistory>(hhId)?.defineWinnerPositions()
realm.findById<HandHistory>(hhId)?.defineWinnerPositions() // realm.executeTransactionAsync { execRealm ->
} // execRealm.copyToRealmOrUpdate(handHistory)
realm.close() // }
} // realm.close()
c.await() // }
} //
// }
}
// Card Centralizer // Card Centralizer
@ -1013,16 +1002,16 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
fun playerSelected(player: Player) { fun playerSelected(player: Player) {
// Remove all use of the selected player // Remove all use of the selected player
this.handHistory.playerSetups.filter { it.player == player }.forEach { this.handHistory.playerSetups.filter { it.player?.id == player.id }.forEach {
it.player = null it.player = null
} }
// Affects the player to the selected position // Affects the player to the selected position
this.tappedPlayerPositionIndex?.let { positionIndex -> this.tappedPlayerPositionIndex?.let { positionIndex ->
player.realm.executeTransaction { // player.realm.executeTransaction {
val ps = this.handHistory.playerSetupForPosition(positionIndex) ?: this.handHistory.createPlayerSetup(positionIndex) val ps = this.handHistory.playerSetupForPosition(positionIndex) ?: this.handHistory.createPlayerSetup(positionIndex)
ps.player = player ps.player = player
} // }
} ?: throw PAIllegalStateException("Click position not set for player selection") } ?: throw PAIllegalStateException("Click position not set for player selection")
} }
@ -1036,20 +1025,20 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
/*** /***
* Tries to deletes the row at the given [position] * Tries to deletes the row at the given [position]
*/ */
fun deleteIfPossible(position: Int) { // fun deleteIfPossible(position: Int) {
when (val row = this.rowRepresentables[position]) { // when (val row = this.rowRepresentables[position]) {
is PlayerSetupRow -> { // is PlayerSetupRow -> {
val playerSetup = this.handHistory.playerSetupForPosition(row.positionIndex) ?: throw PAIllegalStateException("Attempt to delete an null object") // val playerSetup = this.handHistory.playerSetupForPosition(row.positionIndex) ?: throw PAIllegalStateException("Attempt to delete an null object")
this.handHistory.playerSetups.remove(playerSetup) // this.handHistory.playerSetups.remove(playerSetup)
//
this.handHistory.realm?.let { // this.handHistory.realm?.let {
it.executeTransaction { // it.executeTransaction {
playerSetup.deleteFromRealm() // playerSetup.deleteFromRealm()
} // }
} // }
} // }
} // }
} // }
fun removePlayerSetup(positionIndex: Int) { fun removePlayerSetup(positionIndex: Int) {
val ps = this.handHistory.playerSetupForPosition(positionIndex) val ps = this.handHistory.playerSetupForPosition(positionIndex)

@ -13,7 +13,9 @@ import android.provider.MediaStore
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegKit
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
@ -26,373 +28,323 @@ import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext
enum class FileType(var value: String) { enum class FileType(var value: String) {
IMAGE_GIF("image/gif"), IMAGE_GIF("image/gif"),
VIDEO_MP4("video/mp4") VIDEO_MP4("video/mp4")
} }
class ReplayExportService : Service() { class ReplayExportService : Service() {
private lateinit var handHistoryId: String private lateinit var handHistoryId: String
private val binder = LocalBinder() private val binder = LocalBinder()
private val coroutineContext: CoroutineContext override fun onBind(intent: Intent?): IBinder {
get() = Dispatchers.Main return binder
}
override fun onBind(intent: Intent?): IBinder { inner class LocalBinder : Binder() {
return binder fun getService(): ReplayExportService = this@ReplayExportService
} }
inner class LocalBinder : Binder() { fun videoExport(handHistoryId: String) {
fun getService(): ReplayExportService = this@ReplayExportService this@ReplayExportService.handHistoryId = handHistoryId
} if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startFFMPEGVideoExport()
} else {
startFFMPEGVideoExportPreQ()
}
}
fun videoExport(handHistoryId: String) { fun gifExport(handHistoryId: String) {
this@ReplayExportService.handHistoryId = handHistoryId this@ReplayExportService.handHistoryId = handHistoryId
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startFFMPEGVideoExport() startGIFExport()
} else { } else {
startFFMPEGVideoExportPreQ() startGIFExportPreQ()
} }
} }
fun gifExport(handHistoryId: String) { private fun startGIFExport() {
this@ReplayExportService.handHistoryId = handHistoryId
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startGIFExport()
} else {
startGIFExportPreQ()
}
}
private fun startGIFExport() { CoroutineScope(Dispatchers.Default).launch {
GlobalScope.launch(coroutineContext) { val realm = Realm.getDefaultInstance()
val c = GlobalScope.async { realm.refresh()
val realm = Realm.getDefaultInstance() val handHistory = realm.findById<HandHistory>(handHistoryId)
realm.refresh() ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") val context = this@ReplayExportService
val context = this@ReplayExportService val animator = ReplayerAnimator(handHistory, true)
val animator = ReplayerAnimator(handHistory, true) val square = 1024
val square = 1024 val width = square
val height = square
val width = square animator.configure(width.toFloat(), height.toFloat(), context)
val height = square
animator.configure(width.toFloat(), height.toFloat(), context) val formattedDate = Date().dateTimeFileFormatted
val fileName = "hand_${formattedDate}"
val formattedDate = Date().dateTimeFileFormatted // Add a specific media item.
val fileName = "hand_${formattedDate}" val resolver = applicationContext.contentResolver
// Add a specific media item. // Q version tested before calling the function
val resolver = applicationContext.contentResolver val imageCollection =
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// Q version tested before calling the function val gifDetails = ContentValues().apply {
val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, FileType.IMAGE_GIF.value)
}
val gifDetails = ContentValues().apply { val uri = resolver.insert(imageCollection, gifDetails)
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, FileType.IMAGE_GIF.value)
}
val uri = resolver.insert(imageCollection, gifDetails) if (uri != null) {
if (uri != null) { val os = resolver.openOutputStream(uri)
val os = resolver.openOutputStream(uri) val writer = AnimatedGIFWriter(false)
writer.prepareForWrite(os, width, height)
val writer = AnimatedGIFWriter(false) val drawer = TableDrawer()
writer.prepareForWrite(os, width, height) drawer.configurePaints(context, animator)
val drawer = TableDrawer() var animationCount = 0
drawer.configurePaints(context, animator) animator.frames(context) { bitmap, count ->
var animationCount = 0 when {
animator.frames(context) { bitmap, count -> count > 10 -> {
writer.writeFrame(os, bitmap, count * 8)
animationCount = 0
}
else -> {
if (animationCount % 2 == 0) {
writer.writeFrame(os, bitmap)
}
animationCount++
}
}
}
writer.finishWrite(os)
when { realm.close()
count > 10 -> { notifyUser(uri, FileType.IMAGE_GIF)
writer.writeFrame(os, bitmap, count * 8) } else {
animationCount = 0 Timber.w("Resolver insert ended without uri...")
} }
else -> { }
if (animationCount % 2 == 0) {
writer.writeFrame(os, bitmap)
}
animationCount++
}
}
}
writer.finishWrite(os)
realm.close() }
notifyUser(uri, FileType.IMAGE_GIF)
} else {
Timber.w("Resolver insert ended without uri...")
}
}
c.await()
}
} private fun startFFMPEGVideoExport() {
private fun startFFMPEGVideoExport() { CoroutineScope(Dispatchers.Default).launch {
GlobalScope.launch(coroutineContext) { val realm = Realm.getDefaultInstance()
val async = GlobalScope.async { val handHistory = realm.findById<HandHistory>(handHistoryId)
?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
val realm = Realm.getDefaultInstance() val context = this@ReplayExportService
val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
val context = this@ReplayExportService val animator = ReplayerAnimator(handHistory, true)
val animator = ReplayerAnimator(handHistory, true) val square = 1024
val square = 1024 val width = square
val height = square
val width = square animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService)
val height = square val drawer = TableDrawer()
drawer.configurePaints(context, animator)
animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) // generates all images and file descriptor
val drawer = TableDrawer() Timber.d("Generating images for video...")
drawer.configurePaints(context, animator) val tmpDir = animator.generateVideoContent(this@ReplayExportService)
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE"
// generates all images and file descriptor val formattedDate = Date().dateTimeFileFormatted
Timber.d("Generating images for video...") val fileName = "hand_${formattedDate}.mp4"
val tmpDir = animator.generateVideoContent(this@ReplayExportService)
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE"
val formattedDate = Date().dateTimeFileFormatted val outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)
val fileName = "hand_${formattedDate}.mp4" ?: throw PAIllegalStateException("File is invalid")
val output = "${outputDirectory.path}/$fileName"
val outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) ?: throw PAIllegalStateException("File is invalid") Timber.d("Assembling images for video...")
val output = "${outputDirectory.path}/$fileName"
Timber.d("Assembling images for video...") val command =
"-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -vf fps=20 -pix_fmt yuv420p $output"
FFmpegKit.executeAsync(command) {
val command = "-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -vf fps=20 -pix_fmt yuv420p $output" when {
FFmpegKit.executeAsync(command) { it.returnCode.isSuccess -> {
Timber.d("FFMPEG command execution completed successfully")
}
it.returnCode.isCancel -> {
Timber.d("Command execution cancelled by user.")
}
else -> {
Timber.d("Command execution failed with rc=${it.returnCode.value} and the output below.")
}
}
when { File(dpath).delete()
it.returnCode.isSuccess -> { tmpDir.delete()
Timber.d("FFMPEG command execution completed successfully")
}
it.returnCode.isCancel -> {
Timber.d("Command execution cancelled by user.")
}
else -> {
Timber.d(String.format("Command execution failed with rc=%d and the output below.", it.returnCode.value))
}
}
File(dpath).delete() val file = File(output)
tmpDir.delete()
val file = File(output) val resolver = applicationContext.contentResolver
val resolver = applicationContext.contentResolver // Q version tested before calling the function
val videoCollection =
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// Q version tested before calling the function val fileDetails = ContentValues().apply {
val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) Timber.d("set file details = $fileName")
put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, FileType.VIDEO_MP4.value)
}
val fileDetails = ContentValues().apply { // copy video to nice path
Timber.d("set file details = $fileName") resolver.insert(videoCollection, fileDetails)?.let { uri ->
put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, FileType.VIDEO_MP4.value)
}
// copy video to nice path Timber.d("copy file at uri = $uri")
resolver.insert(videoCollection, fileDetails)?.let { uri ->
Timber.d("copy file at uri = $uri") val os = resolver.openOutputStream(uri)
os?.write(file.readBytes())
os?.close()
val os = resolver.openOutputStream(uri) file.delete() // delete temp file
os?.write(file.readBytes())
os?.close()
file.delete() // delete temp file notifyUser(uri, FileType.VIDEO_MP4)
notifyUser(uri, FileType.VIDEO_MP4) } ?: run {
Timber.w("Resolver insert ended without uri...")
}
} ?: run { }
Timber.w("Resolver insert ended without uri...")
}
} }
} }
async.await()
}
} private fun startGIFExportPreQ() {
// private fun startVideoExport() { CoroutineScope(Dispatchers.Default).launch {
//
// GlobalScope.launch(coroutineContext) {
// val c = GlobalScope.async {
//
// val realm = Realm.getDefaultInstance()
// val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
//
// val context = this@ReplayExportService
//
// val animator = ReplayerAnimator(handHistory, true)
//
// val square = 1024
//
// val width = square
// val height = square
//
// animator.setDimension(width.toFloat(), height.toFloat())
// TableDrawer.configurePaints(context, animator)
//
// val muxer = MMediaMuxer()
// muxer.init(null, width, height, "hhVideo", "YES!")
//
// animator.frames(context) { bitmap, count ->
//
// try {
// val byteArray = bitmap.toByteArray()
// muxer.addFrame(byteArray, count, false)
// } catch (e: Exception) {
// Timber.e("error = ${e.message}")
// }
//
// }
//
// realm.close()
//
// muxer.createVideo { path ->
// notifyUser(path)
// }
//
// }
// c.await()
// }
//
// }
private fun startGIFExportPreQ() {
GlobalScope.launch(coroutineContext) {
val c = GlobalScope.async {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh() realm.refresh()
val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") val handHistory = realm.findById<HandHistory>(handHistoryId)
?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
val context = this@ReplayExportService val context = this@ReplayExportService
val animator = ReplayerAnimator(handHistory, true) val animator = ReplayerAnimator(handHistory, true)
val square = 1024 val square = 1024
val width = square val width = square
val height = square val height = square
animator.configure(width.toFloat(), height.toFloat(), context) animator.configure(width.toFloat(), height.toFloat(), context)
val formattedDate = Date().dateTimeFileFormatted val formattedDate = Date().dateTimeFileFormatted
val path = File( val path = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
"hand_${formattedDate}.gif" "hand_${formattedDate}.gif"
).toString() ).toString()
val writer = AnimatedGIFWriter(false) val writer = AnimatedGIFWriter(false)
val os = FileOutputStream(path) val os = FileOutputStream(path)
writer.prepareForWrite(os, width, height) writer.prepareForWrite(os, width, height)
val drawer = TableDrawer() val drawer = TableDrawer()
drawer.configurePaints(context, animator) drawer.configurePaints(context, animator)
var animationCount = 0 var animationCount = 0
animator.frames(context) { bitmap, count -> animator.frames(context) { bitmap, count ->
when { when {
count > 10 -> { count > 10 -> {
writer.writeFrame(os, bitmap, count * 8) writer.writeFrame(os, bitmap, count * 8)
animationCount = 0 animationCount = 0
} }
else -> { else -> {
if (animationCount % 2 == 0) { if (animationCount % 2 == 0) {
writer.writeFrame(os, bitmap) writer.writeFrame(os, bitmap)
} }
animationCount++ animationCount++
} }
} }
} }
writer.finishWrite(os) writer.finishWrite(os)
realm.close() realm.close()
notifyUser(path) notifyUser(path)
} }
c.await()
}
} }
private fun startFFMPEGVideoExportPreQ() { private fun startFFMPEGVideoExportPreQ() {
GlobalScope.launch(coroutineContext) { CoroutineScope(Dispatchers.Default).launch {
val async = GlobalScope.async {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") realm.refresh() // Fixes crash right below
val context = this@ReplayExportService val handHistory = realm.findById<HandHistory>(handHistoryId)
?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
val animator = ReplayerAnimator(handHistory, true) val context = this@ReplayExportService
val square = 1024 val animator = ReplayerAnimator(handHistory, true)
val width = square val square = 1024
val height = square
animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) val width = square
val drawer = TableDrawer() val height = square
drawer.configurePaints(context, animator)
// generates all images and file descriptor animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService)
Timber.d("Generating images for video...") val drawer = TableDrawer()
val tmpDir = animator.generateVideoContent(this@ReplayExportService) drawer.configurePaints(context, animator)
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE"
val formattedDate = Date().dateTimeFileFormatted // generates all images and file descriptor
val output = File( Timber.d("Generating images for video...")
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), val tmpDir = animator.generateVideoContent(this@ReplayExportService)
"hand_${formattedDate}.mp4" val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE"
).path
Environment.getExternalStorageState(tmpDir) val formattedDate = Date().dateTimeFileFormatted
val output = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
"hand_${formattedDate}.mp4"
).path
Timber.d("Assembling images for video...") Environment.getExternalStorageState(tmpDir)
Timber.d("Assembling images for video...")
val command = "-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -vf fps=20 -pix_fmt yuv420p $output" val command =
FFmpegKit.executeAsync(command) { "-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -vf fps=20 -pix_fmt yuv420p $output"
FFmpegKit.executeAsync(command) {
when { when {
it.returnCode.isSuccess -> { it.returnCode.isSuccess -> {
Timber.d("FFMPEG command execution completed successfully") Timber.d("FFMPEG command execution completed successfully")
} }
it.returnCode.isCancel -> { it.returnCode.isCancel -> {
Timber.d("Command execution cancelled by user.") Timber.d("Command execution cancelled by user.")
} }
else -> { else -> {
Timber.d(String.format("Command execution failed with rc=%d and the output below.", it.returnCode.value)) Timber.d("Command execution failed with rc=${it.returnCode.value} and the output below.")
} }
} }
// FFmpeg.executeAsync("-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -vf fps=20 -pix_fmt yuv420p $output") { id, rc -> // FFmpeg.executeAsync("-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -vf fps=20 -pix_fmt yuv420p $output") { id, rc ->
// //
@ -403,61 +355,60 @@ class ReplayExportService : Service() {
// } else { // } else {
// Timber.d(String.format("Command execution failed with rc=%d and the output below.", rc)) // Timber.d(String.format("Command execution failed with rc=%d and the output below.", rc))
// } // }
// Delete descriptor and image files // Delete descriptor and image files
// tmpDir.delete() // tmpDir.delete()
// File(dpath).delete() // File(dpath).delete()
notifyUser(output) notifyUser(output)
} }
} }
async.await()
}
} }
private fun notifyUser(uri: Uri, type: FileType) { private fun notifyUser(uri: Uri, type: FileType) {
val title = getString(R.string.video_available) val title = getString(R.string.video_available)
val body = getString(R.string.video_retrieval_message) + ": " + uri.path val body = getString(R.string.video_retrieval_message) + ": " + uri.path
this.showNotification(title, body, uri, type.value) this.showNotification(title, body, uri, type.value)
} }
private fun notifyUser(path: String) { private fun notifyUser(path: String) {
val title = getString(R.string.video_available) val title = getString(R.string.video_available)
val body = getString(R.string.video_retrieval_message) + ": " + path val body = getString(R.string.video_retrieval_message) + ": " + path
Timber.d("Show local notification, path of file: ${path}") Timber.d("Show local notification, path of file: ${path}")
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
this, this,
this.applicationContext.packageName.toString() + ".fileprovider", this.applicationContext.packageName.toString() + ".fileprovider",
File(path) File(path)
) )
val type = when { val type = when {
path.contains("gif") -> "image/gif" path.contains("gif") -> "image/gif"
else -> "video/*" else -> "video/*"
} }
this.showNotification(title, body, uri, type) this.showNotification(title, body, uri, type)
} }
private fun showNotification(title: String, body: String, uri: Uri, type: String) { private fun showNotification(title: String, body: String, uri: Uri, type: String) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, type) intent.setDataAndType(uri, type)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(intent, getString(R.string.open_file_with)) val chooser = Intent.createChooser(intent, getString(R.string.open_file_with))
val pendingIntent = PendingIntent.getActivity(this, 0, chooser, PendingIntent.FLAG_CANCEL_CURRENT) val pendingIntent =
PendingIntent.getActivity(this, 0, chooser, PendingIntent.FLAG_CANCEL_CURRENT)
TriggerNotification(this, title, body, pendingIntent) TriggerNotification(this, title, body, pendingIntent)
} }
} }

@ -12,8 +12,7 @@ import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Dispatchers
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.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager
@ -38,6 +37,7 @@ import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.viewmodel.AddedDataViewModel
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
@ -48,6 +48,10 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
private lateinit var model: SessionViewModel private lateinit var model: SessionViewModel
private val addedDataViewModel: AddedDataViewModel by lazy {
ViewModelProvider(requireActivity()).get(AddedDataViewModel::class.java)
}
companion object { companion object {
const val TIMER_DELAY = 5000L const val TIMER_DELAY = 5000L
const val REQUEST_CODE_NEW_CUSTOM_FIELD = 1000 const val REQUEST_CODE_NEW_CUSTOM_FIELD = 1000
@ -104,13 +108,21 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
loadOrCreateSession() loadOrCreateSession()
initUI() initUI()
initData()
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
this.addedDataViewModel.data.removeObservers(this)
_binding = null _binding = null
} }
private fun initData() {
this.addedDataViewModel.data.observeForever {
this.onRowValueChanged(it, this.addedDataViewModel.dataIdentifier)
}
}
/** /**
* Init UI * Init UI
*/ */
@ -178,40 +190,44 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
if (sessionId != null) { if (sessionId != null) {
val sessionRealm = realm.findById<Session>(sessionId) val sessionRealm = realm.findById<Session>(sessionId)
if (sessionRealm != null) { sessionRealm?.let {
val copy = realm.copyFromRealm(it)
if (this.model.duplicate) { // duplicate session if (this.model.duplicate) { // duplicate session
realm.executeTransaction { // realm.executeTransaction {
val session = sessionRealm.duplicate() val session = copy.duplicate()
currentSession = session currentSession = session
} // }
sessionHasBeenUserCustomized = false sessionHasBeenUserCustomized = false
} else { // show existing session } else { // show existing session
currentSession = sessionRealm currentSession = copy
sessionHasBeenUserCustomized = true sessionHasBeenUserCustomized = true
} }
} else {
throw PAIllegalStateException("Session cannot be null here, session id = $sessionId") } ?: throw PAIllegalStateException("Session cannot be null here, session id = $sessionId")
}
} else { // create new session } else { // create new session
realm.executeTransaction { executeRealm ->
currentSession = Session.newInstance(executeRealm, this.model.isTournament) currentSession = Session.newInstance(realm, this.model.isTournament)
FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, null, requireContext()) FavoriteSessionFinder.copyParametersFromFavoriteSession(realm, currentSession, null, requireContext())
}
// realm.executeTransaction { executeRealm ->
// currentSession = Session.newInstance(executeRealm, this.model.isTournament)
// FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, null, requireContext())
// }
// Find the nearest location around the user // Find the nearest location around the user
parentActivity?.findNearestLocation { parentActivity?.findNearestLocation {
it?.let { location -> it?.let { location ->
realm.executeTransaction { executeRealm -> // realm.executeTransaction { executeRealm ->
val realmLocation = executeRealm.findById<Location>(location.id) val realmLocation = realm.findById<Location>(location.id)
FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, realmLocation, requireContext()) FavoriteSessionFinder.copyParametersFromFavoriteSession(realm, currentSession, realmLocation, requireContext())
currentSession.location = realmLocation
currentSession.location = realmLocation // }
}
this.updateSessionUI(true) this.updateSessionUI(true)
} }
} }
sessionHasBeenUserCustomized = false this.sessionHasBeenUserCustomized = false
} }
} }
@ -221,6 +237,8 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
return return
} }
this.addedDataViewModel.dataIdentifier = row
val session = this.currentSession val session = this.currentSession
val data = this.editDescriptors(row) val data = this.editDescriptors(row)
@ -253,15 +271,25 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
} }
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
Timber.d("value changed: $value, row: $row")
this.sessionHasBeenUserCustomized = true this.sessionHasBeenUserCustomized = true
try {
getRealm().executeTransaction { updateSessionThenSaveAsynchronously { session ->
this.currentSession.updateValue(value, row) session.updateValue(value, row)
}
} catch (e: PAIllegalStateException) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
return
} }
Timber.d("val = ${this.currentSession.customFieldEntries}")
// try {
// this.currentSession.updateValue(value, row)
// getRealm().executeTransactionAsync { realm ->
// realm.copyToRealmOrUpdate(this.currentSession)
// }
// } catch (e: PAIllegalStateException) {
// Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
// return
// }
this.sessionAdapter.refreshRow(row) this.sessionAdapter.refreshRow(row)
when (row) { when (row) {
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE, SessionPropertiesRow.NET_RESULT, SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE, SessionPropertiesRow.NET_RESULT,
@ -275,6 +303,24 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
} }
private fun updateSessionThenSaveAsynchronously(handler: (Session) -> (Unit)) {
try {
handler(this.currentSession)
} catch (e: PAIllegalStateException) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
return
}
getRealm().executeTransactionAsync { realm ->
val session = realm.copyToRealmOrUpdate(this.currentSession)
session.preCompute()
for (customField in this.model.customFields) {
realm.insertOrUpdate(customField)
}
}
}
/** /**
* Update the UI with the session data * Update the UI with the session data
* Should be called after the initialization of the session * Should be called after the initialization of the session
@ -388,12 +434,20 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
} }
} }
currentSession.startOrContinue() updateSessionThenSaveAsynchronously { session ->
session.startOrContinue()
}
// currentSession.startOrContinue()
binding.recyclerView.smoothScrollToPosition(0) binding.recyclerView.smoothScrollToPosition(0)
} }
SessionState.STARTED -> { SessionState.STARTED -> {
currentSession.pause() updateSessionThenSaveAsynchronously { session ->
session.pause()
}
// currentSession.pause()
} }
else -> { else -> {
} }
@ -406,14 +460,13 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
Timber.d("Start optimal duration finding attempt...") Timber.d("Start optimal duration finding attempt...")
val isLive = this.currentSession.isLive val isLive = this.currentSession.isLive
CoroutineScope(coroutineContext).launch { CoroutineScope(Dispatchers.Default).launch {
var optimalDuration: Double? = null val optimalDuration: Double? = CashGameOptimalDurationCalculator.start(isLive)
// optimalDuration =
val cr = GlobalScope.async { // val cr = GlobalScope.async {
optimalDuration = CashGameOptimalDurationCalculator.start(isLive) // }
} // cr.await()
cr.await()
if (!isDetached) { if (!isDetached) {
optimalDuration?.let { optimalDuration?.let {
@ -421,10 +474,13 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
val delay = it.toLong() //5000L val delay = it.toLong() //5000L
currentSession.scheduleStopNotification(requireContext(), delay) currentSession.scheduleStopNotification(requireContext(), delay)
val formattedDuration = (it / 3600 / 1000).formattedHourlyDuration() launch(Dispatchers.Main) {
Timber.d("Setting stop notification in: $formattedDuration") val formattedDuration = (it / 3600 / 1000).formattedHourlyDuration()
val message = requireContext().getString(R.string.stop_notification_in_, formattedDuration) Timber.d("Setting stop notification in: $formattedDuration")
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() val message = requireContext().getString(R.string.stop_notification_in_, formattedDuration)
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
}
} }
} }
} }
@ -435,7 +491,9 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
* Stop the current session * Stop the current session
*/ */
private fun stopSession() { private fun stopSession() {
this.currentSession.stop(requireContext()) updateSessionThenSaveAsynchronously { session ->
session.stop(requireContext())
}
updateSessionUI() updateSessionUI()
} }
@ -454,7 +512,9 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
* Restart timer * Restart timer
*/ */
private fun restartTimer() { private fun restartTimer() {
currentSession.restart() updateSessionThenSaveAsynchronously { session ->
session.restart()
}
updateSessionUI() updateSessionUI()
} }
@ -478,22 +538,30 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
val bankrollId = this.currentSession.bankroll?.id val bankrollId = this.currentSession.bankroll?.id
this.currentSession.delete() val sessionId = this.currentSession.id
getRealm().executeTransactionAsync { asyncRealm ->
asyncRealm.findById<Session>(sessionId)?.delete()
}
// this.currentSession.delete()
bankrollId?.let { bankrollId?.let {
BankrollReportManager.notifyBankrollReportImpact(bankrollId) BankrollReportManager.notifyBankrollReportImpact(bankrollId)
} }
activity?.finish() activity?.finish()
} }
/** // /**
* Called when the user pressed back on the parent activity // * Called when the user pressed back on the parent activity
*/ // */
override fun onBackPressed() { // override fun onBackPressed() {
super.onBackPressed() // super.onBackPressed()
if (!sessionHasBeenUserCustomized) { // if (!sessionHasBeenUserCustomized) {
currentSession.delete() // val sessionId = currentSession.id
} // getRealm().executeTransactionAsync { asyncRealm ->
} // asyncRealm.findById<Session>(sessionId)?.delete()
// }
// }
// }
//// Static Data Source //// Static Data Source
@ -641,15 +709,16 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
) )
) )
is CustomField -> { is CustomField -> {
val entry = row.entries.intersect(session.customFieldEntries).firstOrNull()
row.editingDescriptors( row.editingDescriptors(
when (row.type) { when (row.type) {
CustomField.Type.LIST.uniqueIdentifier -> mapOf( CustomField.Type.LIST.uniqueIdentifier -> {
"defaultValue" to session.customFieldEntries.find { it.customField?.id == row.id }?.value, mapOf(
"data" to row.entries "defaultValue" to entry?.value,
) "data" to row.entries
else -> mapOf( )
"defaultValue" to session.customFieldEntries.find { it.customField?.id == row.id }?.numericValue }
) else -> mapOf("defaultValue" to entry?.numericValue)
} }
) )
} }
@ -659,7 +728,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun resultCaptureTypeSelected(resultCaptureType: ResultCaptureType, applyBankroll: Boolean) { override fun resultCaptureTypeSelected(resultCaptureType: ResultCaptureType, applyBankroll: Boolean) {
getRealm().executeTransaction { // cleanup existing results updateSessionThenSaveAsynchronously {
when (resultCaptureType) { when (resultCaptureType) {
ResultCaptureType.NET_RESULT -> { ResultCaptureType.NET_RESULT -> {
this.currentSession.clearBuyinCashedOut() this.currentSession.clearBuyinCashedOut()

@ -46,6 +46,11 @@ class SessionViewModel : ViewModel() {
*/ */
var rows: List<RowRepresentable> = mutableListOf() var rows: List<RowRepresentable> = mutableListOf()
/**
* The list of customFields
*/
var customFields: List<CustomField> = listOf()
fun rowForPosition(position: Int): RowRepresentable { fun rowForPosition(position: Int): RowRepresentable {
return this.rows[position] return this.rows[position]
} }
@ -104,7 +109,10 @@ class SessionViewModel : ViewModel() {
// Add custom fields // Add custom fields
rows.add(SeparatorRow()) rows.add(SeparatorRow())
rows.addAll(realm.sorted<CustomField>())
val customFields = realm.sorted<CustomField>()
this.customFields = customFields.map { realm.copyFromRealm(it) }
rows.addAll(this.customFields)
this.rows = rows this.rows = rows
} }

@ -49,24 +49,28 @@ class DealtHandsPerHourFragment : RealmFragment() {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
val userConfig = UserConfig.getConfiguration(this.getRealm()) UserConfig.getConfiguration(this.getRealm()) { userConfig ->
this.liveValue.hint = "${userConfig.liveDealtHandsPerHour}" this.liveValue.hint = "${userConfig.liveDealtHandsPerHour}"
this.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}" this.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}"
}
} }
private fun save() { private fun save() {
getRealm().executeTransaction { realm -> getRealm().executeTransactionAsync { realm ->
val userConfig = UserConfig.getConfiguration(realm) UserConfig.getConfiguration(realm) { userConfig ->
this.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour -> this.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour ->
userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour
} }
this.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour -> this.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour ->
userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour
}
realm.insertOrUpdate(userConfig)
} }
realm.copyToRealmOrUpdate(userConfig)
// val userConfig = UserConfig.getConfiguration(realm)
// update all precomputed hand counts // update all precomputed hand counts
realm.where(ComputableResult::class.java).findAll().forEach { cr -> realm.where(ComputableResult::class.java).findAll().forEach { cr ->

@ -90,16 +90,18 @@ class TransactionFilterFragment : RealmFragment(), StaticRowRepresentableDataSou
.findAll() .findAll()
this.model.transactionTypes = transactionTypes this.model.transactionTypes = transactionTypes
val userConfig = UserConfig.getConfiguration(this.getRealm()) UserConfig.getConfiguration(this.getRealm()) { userConfig ->
this.model.selectedTransactionTypes = userConfig.transactionTypes(getRealm()).toMutableSet() this.model.selectedTransactionTypes = userConfig.transactionTypes(getRealm()).toMutableSet()
}
} }
private fun save() { private fun save() {
getRealm().executeTransaction { realm -> getRealm().executeTransactionAsync { asyncRealm ->
val userConfig = UserConfig.getConfiguration(realm) UserConfig.getConfiguration(asyncRealm) { userConfig ->
userConfig.setTransactionTypeIds(this.model.selectedTransactionTypes) userConfig.setTransactionTypeIds(this.model.selectedTransactionTypes)
realm.copyToRealmOrUpdate(userConfig) asyncRealm.insertOrUpdate(userConfig)
}
} }
this.activity?.finish() this.activity?.finish()
} }

@ -34,7 +34,6 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.extensions.ChipGroupExtension import net.pokeranalytics.android.ui.extensions.ChipGroupExtension
import net.pokeranalytics.android.ui.extensions.px import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.extensions.setTextFormat import net.pokeranalytics.android.ui.extensions.setTextFormat
import net.pokeranalytics.android.ui.fragment.PerformanceRow
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.graph.setStyle import net.pokeranalytics.android.ui.graph.setStyle
import net.pokeranalytics.android.ui.modules.bankroll.BankrollRowRepresentable import net.pokeranalytics.android.ui.modules.bankroll.BankrollRowRepresentable
@ -675,30 +674,6 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
BindableHolder { BindableHolder {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
if (row is PerformanceRow) {
itemView.findViewById<AppCompatTextView>(R.id.title)?.let {
it.text = row.localizedTitle(itemView.context)
}
itemView.findViewById<AppCompatTextView>(R.id.value)?.let {
it.text = adapter.dataSource.charSequenceForRow(row, itemView.context)
}
itemView.findViewById<AppCompatImageView>(R.id.badge)?.let {
it.isVisible = adapter.dataSource.boolForRow(row)
}
itemView.findViewById<AppCompatImageView>(R.id.nextArrow)?.let {
it.visibility = if (row.report.hasGraph) {
View.VISIBLE
} else {
View.GONE
}
}
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
itemView.setOnClickListener(listener)
}
} }
} }

@ -107,6 +107,14 @@ sealed class StaticReport(override var uniqueIdentifier: Int) : RowRepresentable
} }
val stats: List<Stat> val stats: List<Stat>
get() {
return when (this) {
OptimalDuration -> listOf(Stat.AVERAGE_NET_BB)
else -> listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.NUMBER_OF_GAMES)
}
}
val performanceStats: List<Stat>
get() { get() {
return when (this) { return when (this) {
OptimalDuration -> listOf(Stat.AVERAGE_NET_BB) OptimalDuration -> listOf(Stat.AVERAGE_NET_BB)

@ -3,15 +3,18 @@ package net.pokeranalytics.android.ui.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException
import net.pokeranalytics.android.model.Stakes import net.pokeranalytics.android.model.Stakes
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.extensions.findById
import java.util.* import java.util.*
class BottomSheetViewModelFactory(var row: RowRepresentable, var delegate: RowRepresentableDelegate): ViewModelProvider.Factory { class BottomSheetViewModelFactory(var row: RowRepresentable, var delegate: RowRepresentableDelegate): ViewModelProvider.Factory {
@ -84,7 +87,7 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
var alternativeLabels: Boolean = false var alternativeLabels: Boolean = false
/** /**
* Multiselection * Multi-selection
*/ */
val selectedRows: ArrayList<RowRepresentable> = ArrayList() val selectedRows: ArrayList<RowRepresentable> = ArrayList()
@ -224,12 +227,31 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
fun isSelected(row: RowRepresentable): Boolean { fun isSelected(row: RowRepresentable): Boolean {
return this.selectedRows.contains(row) return this.selectedRows.contains(row)
// return when(row.bottomSheetType) {
// BottomSheetType.MULTI_SELECTION -> {
// val identifiables = this.selectedRows as List<Identifiable>
// val ids = identifiables.map { it.id }
// val id = (row as Identifiable).id
// ids.contains(id)
// }
// else -> this.selectedRows.contains(row)
// }
} }
fun changedValue(): Any? { fun changedValue(): Any? {
return when(row.bottomSheetType) { return when(row.bottomSheetType) {
BottomSheetType.DOUBLE_EDIT_TEXT -> arrayListOf(this.stringValue, this.secondStringValue) BottomSheetType.DOUBLE_EDIT_TEXT -> arrayListOf(this.stringValue, this.secondStringValue)
BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> arrayListOf(this.someValues[0], this.someValues[1]) BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> arrayListOf(this.someValues[0], this.someValues[1])
BottomSheetType.MULTI_SELECTION -> {
this.realmData?.realm?.let { realm ->
val identifiables = this.selectedRows as List<Identifiable>
identifiables.firstOrNull()?.realmObjectClass?.let { clazz ->
val ids = identifiables.map { it.id }
val objects = ids.mapNotNull { realm.findById(clazz, it) }
objects.map { realm.copyFromRealm(it) }
} ?: kotlin.run { this.selectedRows }
} ?: kotlin.run { this.selectedRows }
}
else -> getValue() else -> getValue()
} }
} }
@ -242,9 +264,15 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
} }
} }
fun rowSelected(position: Int): RowRepresentable? { fun rowSelected(position: Int): RowRepresentable {
return when(this.row.bottomSheetType) { return when(this.row.bottomSheetType) {
BottomSheetType.LIST -> this.realmData?.get(position) BottomSheetType.LIST -> {
realmData?.realm?.let { realm ->
this.realmData?.get(position)?.let {
realm.copyFromRealm(it as RealmModel) as? RowRepresentable
} ?: throw PAIllegalStateException("item not found at $position")
} ?: throw PAIllegalStateException("realm not found")
}
BottomSheetType.LIST_STATIC -> this.staticRows[position] BottomSheetType.LIST_STATIC -> this.staticRows[position]
else -> throw PAIllegalStateException("row selected for unmanaged bottom sheet type") else -> throw PAIllegalStateException("row selected for unmanaged bottom sheet type")
} }

@ -1,9 +1,21 @@
package net.pokeranalytics.android.ui.viewmodel package net.pokeranalytics.android.ui.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.ui.view.RowRepresentable
open class AddedDataViewModel : ViewModel() {
var dataForAdd = false
lateinit var dataIdentifier: RowRepresentable
var data: MutableLiveData<Identifiable> = MutableLiveData()
}
open class DataManagerViewModel : ViewModel() { open class DataManagerViewModel : ViewModel() {

@ -35,9 +35,9 @@ class FakeDataManager {
val locations = realm.where<Location>().findAll() val locations = realm.where<Location>().findAll()
if (locations.size == 0) { if (locations.size == 0) {
realm.executeTransaction { realm.executeTransactionAsync { asyncRealm ->
listOf("Bellagio", "Aria", "Borgata").map { listOf("Bellagio", "Aria", "Borgata").map {
realm.getOrCreate<Location>(it) asyncRealm.getOrCreate<Location>(it)
} }
} }
} }

@ -5,3 +5,9 @@ const val RANDOM_PLAYER: String = "☺"
const val FFMPEG_DESCRIPTOR_FILE = "descriptor.txt" const val FFMPEG_DESCRIPTOR_FILE = "descriptor.txt"
const val BLIND_SEPARATOR: String = "/" const val BLIND_SEPARATOR: String = "/"
const val UUID_SEPARATOR: String = "," const val UUID_SEPARATOR: String = ","
class Global {
companion object {
const val LAUNCH_ASYNC_LISTENERS = false
}
}

@ -2,15 +2,12 @@ package net.pokeranalytics.android.util
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.* import android.graphics.*
import android.graphics.Paint.FILTER_BITMAP_FLAG
import android.media.ExifInterface import android.media.ExifInterface
import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import timber.log.Timber import timber.log.Timber
@ -270,8 +267,8 @@ object ImageUtils {
* Save the bitmap in a file * Save the bitmap in a file
*/ */
fun saveBitmapInFile(context: Context, bitmap: Bitmap, filename: String, action: (filePath: String) -> Unit) { fun saveBitmapInFile(context: Context, bitmap: Bitmap, filename: String, action: (filePath: String) -> Unit) {
GlobalScope.launch { CoroutineScope(Dispatchers.Default).launch {
val outputFile = File(context.filesDir, filename) val outputFile = File(context.filesDir, filename)
@ -291,7 +288,7 @@ object ImageUtils {
} }
} }
GlobalScope.launch(Dispatchers.Main) { launch(Dispatchers.Main) {
Timber.d("Save file here: ${outputFile.absolutePath}") Timber.d("Save file here: ${outputFile.absolutePath}")
action(outputFile.absolutePath) action(outputFile.absolutePath)
} }

@ -0,0 +1,12 @@
package net.pokeranalytics.android.util
fun <T, V> List<T>.intersectBy(other: List<V>, by: (T) -> (V)) : Set<T> {
val results = mutableSetOf<T>()
for (item in this) {
val key = by(item)
if (other.contains(key)) {
results.add(item)
}
}
return results
}

@ -61,7 +61,7 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
if (realm.isInTransaction) { if (realm.isInTransaction) {
this.deleteInsertedFromRealm(realm) this.deleteInsertedFromRealm(realm)
} else { } else {
realm.executeTransaction { realm.executeTransactionAsync {
this.deleteInsertedFromRealm(realm) this.deleteInsertedFromRealm(realm)
} }
} }

@ -21,15 +21,15 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
abstract class PACSVDescriptor<T : Identifiable>(source: DataSource, abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
private var isTournament: Boolean?, private var isTournament: Boolean?,
vararg elements: CSVField) vararg elements: CSVField)
: DataCSVDescriptor<T>(source, *elements) { : DataCSVDescriptor<T>(source, *elements) {
var noSessionImport: Boolean = false private var noSessionImport: Boolean = false
init { init {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
this.noSessionImport = realm.count(Session::class.java) == 0L this.noSessionImport = realm.count(Session::class.java) == 0L
realm.close() realm.close()
} }
@ -39,13 +39,20 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
private var dateFormat: DateFormat? = null private var dateFormat: DateFormat? = null
private fun newSession(isTournament: Boolean): Session {
val session = Session()
session.result = Result()
session.type = if (isTournament) Session.Type.TOURNAMENT.ordinal else Session.Type.CASH_GAME.ordinal
return session
}
/** /**
* Parses a [record] and return an optional Session * Parses a [record] and return an optional Session
*/ */
protected fun parseSession(realm: Realm, record: CSVRecord): Session? { protected fun parseSession(realm: Realm, record: CSVRecord): Session? {
val isTournament = isTournament ?: false val isTournament = isTournament ?: false
val session = Session.newInstance(realm, isTournament, managed = false) val session = this.newSession(isTournament) // Session.newInstance(realm, isTournament)
var startDate: Date? = null var startDate: Date? = null
var endDate: Date? = null var endDate: Date? = null
@ -276,6 +283,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
this.addAdditionallyCreatedIdentifiable(transaction) this.addAdditionallyCreatedIdentifiable(transaction)
} }
managedSession.preCompute()
return managedSession return managedSession
} else { } else {
Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net") Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net")

@ -79,11 +79,11 @@ class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg el
is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition) is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition)
is SessionField.Comment -> data.comment is SessionField.Comment -> data.comment
is SessionField.NumberCustomField -> { is SessionField.NumberCustomField -> {
val entry = data.customFieldEntries.find { it.customField?.id == field.customField.id } val entry = field.customField.entries.intersect(data.customFieldEntries).firstOrNull()
field.format(entry?.numericValue) field.format(entry?.numericValue)
} }
is SessionField.ListCustomField -> { is SessionField.ListCustomField -> {
val entry = data.customFieldEntries.find { it.customField?.id == field.customField.id } val entry = field.customField.entries.intersect(data.customFieldEntries).firstOrNull()
entry?.value entry?.value
} }
else -> null else -> null

@ -1,6 +1,8 @@
package net.pokeranalytics.android.util.extensions package net.pokeranalytics.android.util.extensions
import io.realm.* import io.realm.*
import io.realm.kotlin.where
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.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
@ -62,7 +64,7 @@ fun <T : RealmModel> Realm.sorted(clazz: Class<T>, editableOnly: Boolean = false
query.notEqualTo("id", it) query.notEqualTo("id", it)
} }
val items = query.findAll() val items = query.findAllAsync()
var sortField = arrayOf("name") var sortField = arrayOf("name")
var resultSort = arrayOf(Sort.ASCENDING) var resultSort = arrayOf(Sort.ASCENDING)
@ -96,8 +98,8 @@ inline fun <reified C : RealmModel> Realm.sorted(editableOnly: Boolean = false,
*/ */
fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) { fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) {
val results = this.where(clazz).findAll() this.executeTransactionAsync {
this.executeTransaction { val results = it.where(clazz).findAll()
results.forEach { countableUsage -> results.forEach { countableUsage ->
val countable = (countableUsage as UsageCountable) val countable = (countableUsage as UsageCountable)
@ -146,4 +148,12 @@ fun Realm.lookupForNameInAllTablesById(id: String): String? {
} }
} }
return null return null
} }
fun Realm.computableResult(session: Session): ComputableResult? {
val crs = this.where<ComputableResult>().equalTo("session.id", session.id).findAll()
if (crs.size > 1) {
throw PAIllegalStateException("Session has multiple computable results")
}
return crs.firstOrNull()
}

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

Loading…
Cancel
Save