Compare commits

..

No commits in common. 'master' and 'blinds' have entirely different histories.

  1. 139
      CLAUDE.md
  2. 66
      app/build.gradle
  3. 22
      app/proguard-rules.pro
  4. 4
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  5. 10
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  6. 3
      app/src/debug/AndroidManifest.xml
  7. 115
      app/src/main/AndroidManifest.xml
  8. BIN
      app/src/main/ic_launcher-playstore.png
  9. 33
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  10. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  11. 60
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  12. 47
      app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt
  13. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  14. 2
      app/src/main/java/net/pokeranalytics/android/calcul/AggregationTypeExtensions.kt
  15. 2
      app/src/main/java/net/pokeranalytics/android/calcul/ComputedResultsExtensions.kt
  16. 2
      app/src/main/java/net/pokeranalytics/android/calcul/ReportDisplay.kt
  17. 2
      app/src/main/java/net/pokeranalytics/android/calcul/ReportExtensions.kt
  18. 8
      app/src/main/java/net/pokeranalytics/android/calcul/StatRepresentable.kt
  19. 53
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  20. 18
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  21. 17
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  22. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  23. 17
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  24. 24
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  25. 2
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  26. 35
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  27. 9
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  28. 20
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  29. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  30. 5
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  31. 29
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  32. 151
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  33. 3
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  34. 109
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  35. 67
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  36. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  37. 7
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  38. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  39. 131
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  40. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  41. 4
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  42. 99
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  43. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  44. 64
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  45. 11
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  46. 13
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  47. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  48. 4
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  49. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  50. 115
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  51. 15
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  52. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt
  53. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt
  54. 32
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  55. 187
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt
  56. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt
  57. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt
  58. 40
      app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt
  59. 1
      app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt
  60. 94
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  61. 45
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  62. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  63. 38
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  64. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt
  65. 195
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  66. 184
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  67. 100
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  68. 79
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  69. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/Top10Fragment.kt
  70. 45
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  71. 17
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt
  72. 14
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  73. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  74. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStakesFragment.kt
  75. 27
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt
  76. 28
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  77. 15
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt
  78. 8
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt
  79. 82
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt
  80. 24
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt
  81. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/CustomFieldDataFragment.kt
  82. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt
  83. 80
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt
  84. 153
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataViewModel.kt
  85. 34
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt
  86. 29
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt
  87. 12
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt
  88. 4
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsViewModel.kt
  89. 66
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/editor/EditorAdapter.kt
  90. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/editor/EditorFragment.kt
  91. 24
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/ActionList.kt
  92. 3
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/EditorViewModel.kt
  93. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/FrameManager.kt
  94. 346
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt
  95. 12
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerAnimator.kt
  96. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerView.kt
  97. 7
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/TableDrawer.kt
  98. 7
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/KeyboardAmountView.kt
  99. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionActivity.kt
  100. 15
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,139 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is **Poker Analytics**, a comprehensive Android application for tracking and analyzing poker sessions. The app supports both cash games and tournaments, providing detailed statistics, reporting, and data visualization capabilities.
### Key Features
- Session tracking (cash games and tournaments)
- Advanced filtering and reporting
- Bankroll management
- Hand history import and replay
- Statistical analysis and charting
- Data backup and export functionality
- Multi-currency support
## Development Commands
### Building the Project
```bash
./gradlew assembleStandardRelease # Build release APK
./gradlew assembleStandardDebug # Build debug APK
./gradlew build # Build all variants
```
### Running Tests
```bash
./gradlew test # Run unit tests
./gradlew connectedAndroidTest # Run instrumented tests (requires device/emulator)
./gradlew testStandardDebugUnitTest # Run specific unit tests
```
### Cleaning
```bash
./gradlew clean # Clean build artifacts
```
### Build Configuration
- **Target SDK**: 35 (Android 15)
- **Min SDK**: 23 (Android 6.0)
- **Build Tools**: 30.0.3
- **Kotlin Version**: 1.9.24
- **Realm Schema Version**: 14
## Architecture Overview
### Package Structure
The main source code is organized under `app/src/main/java/net/pokeranalytics/android/`:
#### Core Components
- **`model/`** - Data models and business logic
- `realm/` - Realm database models (Session, Bankroll, Result, etc.)
- `filter/` - Query system for filtering sessions
- `migrations/` - Database migration handling
- `handhistory/` - Hand history data structures
- **`ui/`** - User interface components
- `activity/` - Main activities (HomeActivity, SessionActivity, etc.)
- `fragment/` - UI fragments organized by feature
- `adapter/` - RecyclerView adapters and data sources
- `modules/` - Feature-specific UI modules
- **`calculus/`** - Statistics and calculation engine
- Core calculation logic for poker statistics
- Report generation system
- Performance tracking
- **`util/`** - Utility classes
- `csv/` - CSV import/export functionality
- `billing/` - In-app purchase handling
- `extensions/` - Kotlin extension functions
#### Key Classes
- **`Session`** (`model/realm/Session.kt`): Core session data model
- **`HomeActivity`** (`ui/activity/HomeActivity.kt`): Main app entry point with tab navigation
- **`PokerAnalyticsApplication`**: Application class handling initialization
### Database Architecture
The app uses **Realm** database with these key entities:
- `Session` - Individual poker sessions
- `Bankroll` - Bankroll management
- `Result` - Session results and statistics
- `ComputableResult` - Pre-computed statistics for performance
- `Filter` - Saved filter configurations
- `HandHistory` - Hand-by-hand game data
### UI Architecture
- **MVVM pattern** with Android Architecture Components
- **Fragment-based navigation** with bottom navigation tabs
- **Custom RecyclerView adapters** for data presentation
- **Material Design** components
## Key Technologies
### Core Dependencies
- **Realm Database** (10.15.1) - Local data storage
- **Kotlin Coroutines** - Asynchronous programming
- **Firebase Crashlytics** - Crash reporting and analytics
- **Material Design Components** - UI framework
- **MPAndroidChart** - Data visualization
- **CameraX** - Image capture functionality
### Testing
- **JUnit** for unit testing
- **Android Instrumented Tests** for integration testing
- Test files located in `app/src/androidTest/` and `app/src/test/`
## Development Guidelines
### Working with Sessions
- Sessions are the core data model representing individual poker games
- Use `Session.newInstance()` to create new sessions properly
- Always call `computeStats()` after modifying session data
- Sessions can be in various states: PENDING, STARTED, PAUSED, ENDED
### Database Operations
- Use Realm transactions for data modifications
- The app uses schema version 14 - increment when making schema changes
- Migration logic is in `PokerAnalyticsMigration.kt`
### Testing Data
- Use `FakeDataManager.createFakeSessions()` for generating test data
- Seed data is available through the `Seed` class
### Build Variants
- **standard** - Main production flavor
- Release builds are optimized and obfuscated with ProGuard
## Performance Considerations
- Sessions use `ComputableResult` for pre-computed statistics
- Large datasets are handled with Realm's lazy loading
- Chart rendering is optimized for large data sets
- Background processing uses Kotlin Coroutines
## Security & Privacy
- Sensitive data is encrypted in Realm database
- Crash logging excludes personal information
- Backup functionality includes data encryption

@ -1,14 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android' apply plugin: 'realm-android'
// Crashlytics // Crashlytics
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.crashlytics'
// Serialization //////////////
apply plugin: "kotlinx-serialization"
repositories { repositories {
maven { url 'https://jitpack.io' } // required for MPAndroidChart maven { url 'https://jitpack.io' } // required for MPAndroidChart
@ -17,8 +15,8 @@ repositories {
android { android {
compileSdkVersion 35 compileSdkVersion 32
buildToolsVersion "30.0.3" buildToolsVersion "30.0.2"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -29,13 +27,16 @@ android {
jvmTarget = JavaVersion.VERSION_1_8 jvmTarget = JavaVersion.VERSION_1_8
} }
lintOptions {
disable 'MissingTranslation'
}
defaultConfig { defaultConfig {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 35 targetSdkVersion 32
versionCode 180 versionCode 136
versionName "6.0.38" versionName "5.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -88,10 +89,6 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
@ -99,11 +96,11 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin // Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
// Android // Android
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
@ -112,16 +109,13 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places // Places
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions // Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0' implementation 'com.android.billingclient:billing:3.0.1'
// Import the BoM for the Firebase platform // Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -143,36 +137,16 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export) // ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS' implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
// Image picking and registerForActivityResult
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation "androidx.fragment:fragment-ktx:1.4.1"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests // Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1' androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test:runner:1.6.2' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.6.1' androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
// Test // Test
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.12'
testImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2' testImplementation 'com.android.support.test:rules:1.0.2'

@ -65,25 +65,3 @@
# Enum # Enum
-optimizations !class/unboxing/enum -optimizations !class/unboxing/enum
# Serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class net.pokeranalytics.android.**$$serializer { *; }
-keepclassmembers class net.pokeranalytics.android.** {
*** Companion;
}
-keepclasseswithmembers class net.pokeranalytics.android.** {
kotlinx.serialization.KSerializer serializer(...);
}

@ -186,13 +186,13 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
Assert.fail("No std100 stat") Assert.fail("No std100 stat")
} }
results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let { results.computedStat(Stat.MAXIMUM_NETRESULT)?.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_NET_RESULT)?.let { results.computedStat(Stat.MINIMUM_NETRESULT)?.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")

@ -458,7 +458,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.NetAmountWon() val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) } val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value)) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow)) val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow))
@ -482,7 +482,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.NetAmountWon() val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) } val filterElementRow = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value)) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -506,11 +506,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filterMore = QueryCondition.NetAmountWon() val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) } val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value)) filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val filterLess = QueryCondition.NetAmountWon() val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) } val filterElementRow2 = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value)) filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess)) val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess))
@ -584,7 +584,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10) val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10)
sessions = Filter.queryOn(realm, Query(filterMore)) sessions = Filter.queryOn<Session>(realm, Query(filterMore))
Assert.assertEquals(1, sessions.size) Assert.assertEquals(1, sessions.size)

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.pokeranalytics.android">
<!-- Add WRITE_EXTERNAL_STORAGE only for debug / test --> <!-- Add WRITE_EXTERNAL_STORAGE only for debug / test -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

@ -1,18 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
package="net.pokeranalytics.android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<application <application
android:name=".PokerAnalyticsApplication" android:name=".PokerAnalyticsApplication"
@ -62,186 +59,130 @@
</activity> </activity>
<!-- DatabaseCopyActivity is only used in development for now -->
<!-- <activity android:name=".ui.activity.DatabaseCopyActivity"-->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <data android:scheme="content" />-->
<!-- <data android:scheme="file" />-->
<!-- <data android:mimeType="*/*" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity" android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity" android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity" android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"/>
android:exported="true"/>
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity" android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity" android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity" android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity" android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity" android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity" android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard" android:launchMode="standard"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity" android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity" android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity" android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity" android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity" android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity" android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity" android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog" android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop" android:launchMode="singleTop"/>
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<service <service android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService" android:exported="false"/>
android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -7,15 +7,16 @@ 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.CoroutineScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
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.util.CrashLogging
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -23,9 +24,6 @@ import java.util.*
class PokerAnalyticsApplication : Application() { class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object { companion object {
fun timeSinceInstall(context: Context): Long { fun timeSinceInstall(context: Context): Long {
@ -38,9 +36,7 @@ class PokerAnalyticsApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this) FirebaseApp.initializeApp(this)
}
UserDefaults.init(this) UserDefaults.init(this)
@ -51,7 +47,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(12)
.allowWritesOnUiThread(true) .allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
@ -76,24 +72,11 @@ class PokerAnalyticsApplication : Application() {
// this.createFakeSessions() // this.createFakeSessions()
} }
// Patch Patcher.patchAll(this.applicationContext)
Patcher.patchAll(this)
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// 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)
// }
} }
/** /**
@ -106,7 +89,7 @@ class PokerAnalyticsApplication : Application() {
realm.close() realm.close()
if (sessionsCount < 10) { if (sessionsCount < 10) {
CoroutineScope(context = Dispatchers.IO).launch { GlobalScope.launch {
FakeDataManager.createFakeSessions(500) FakeDataManager.createFakeSessions(500)
} }
} }

@ -1,75 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import timber.log.Timber
object RetrofitClient {
private const val BASE_URL = "https://www.pokeranalytics.net/backup/"
fun getClient(): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
class BackupService {
private val retrofit = RetrofitClient.getClient()
val backupApi: MyBackupApi = retrofit.create(MyBackupApi::class.java)
}
interface MyBackupApi {
@Multipart
@POST("send")
fun postFile(@Part mail: MultipartBody.Part, @Part fileBody: MultipartBody.Part): Call<Void>
}
object BackupApi {
private val service = BackupService()
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send
suspend fun backupFile(context: Context, mail: String, fileName: String, fileContent: String): Boolean {
val filePart = MultipartBody.Part.createFormData(
"file",
fileName,
RequestBody.create(MediaType.parse("text/csv"), fileContent)
)
val mailPart = MultipartBody.Part.createFormData("recipient", mail)
return if (context.isNetworkAvailable()) {
var success = false
val job = CoroutineScope(context = Dispatchers.IO).async {
success = try {
val response = service.backupApi.postFile(mailPart, filePart).execute()
Timber.d("response code = ${response.code()}")
Timber.d("success = ${response.isSuccessful}")
true
} catch (e: Exception) {
Timber.d("!!! backup failed: ${e.message}")
CrashLogging.logException(e)
false
}
}
job.await()
return success
} else {
false
}
}
}

@ -1,60 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import androidx.annotation.Keep
import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
@Keep
@Serializable
data class RateResponse(var info: RateInfo)
@Keep
@Serializable
data class RateInfo(var rate: Double)
class CurrencyConverterApi {
companion object {
val json = Json { ignoreUnknownKeys = true }
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
Timber.d("Api call = $url")
val stringRequest = object : StringRequest(
Method.GET, url,
{ response ->
val o = json.decodeFromString<RateResponse>(response)
Timber.d("rate = ${o.info.rate}")
callback(o.info.rate, null)
},
{
Timber.d("Api call failed: ${it.message}")
callback(null, it)
}) {
override fun getHeaders(): MutableMap<String, String> {
val headers = HashMap<String, String>()
headers["apikey"] = "XnfeyID3PMKd3k4zTPW0XmZAbcZlZgqH"
return headers
}
}
queue.add(stringRequest)
}
}
}

@ -0,0 +1,47 @@
package net.pokeranalytics.android.api
import android.content.Context
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import timber.log.Timber
class FreeConverterApi {
companion object {
fun currencyRate(pair: String, context: Context, callback: (Double) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=5ba8d38995282fe8b1c8"
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 }
val stringRequest = StringRequest(
Request.Method.GET, url,
Response.Listener { response ->
val json = Json(JsonConfiguration.Stable)
val f = json.parseJson(response)
f.jsonObject[pair]?.primitive?.double?.let { rate ->
callback(rate)
} ?: run {
Timber.d("no rate: $response")
}
},
Response.ErrorListener {
Timber.d("Api call failed: ${it.message}")
})
queue.add(stringRequest)
}
}
}

@ -1,242 +0,0 @@
package net.pokeranalytics.android.api
import com.android.volley.*
import com.android.volley.toolbox.HttpHeaderParser
import java.io.*
open class VolleyMultipartRequest : Request<NetworkResponse?> {
private val twoHyphens = "--"
private val lineEnd = "\r\n"
private val boundary = "apiclient-" + System.currentTimeMillis()
private var mListener: Response.Listener<NetworkResponse>
private var mErrorListener: Response.ErrorListener
private var mHeaders: Map<String, String>? = null
private var byteData: Map<String, DataPart>? = null
/**
* Default constructor with predefined header and post method.
*
* @param url request destination
* @param headers predefined custom header
* @param listener on success achieved 200 code from request
* @param errorListener on error http or library timeout
*/
constructor(
url: String?, headers: Map<String, String>?,
byteData: Map<String, DataPart>,
listener: Response.Listener<NetworkResponse>,
errorListener: Response.ErrorListener
) : super(Method.POST, url, errorListener) {
mListener = listener
this.mErrorListener = errorListener
mHeaders = headers
this.byteData = byteData
}
/**
* Constructor with option method and default header configuration.
*
* @param method method for now accept POST and GET only
* @param url request destination
* @param listener on success event handler
* @param errorListener on error event handler
*/
constructor(
method: Int, url: String?,
listener: Response.Listener<NetworkResponse>,
errorListener: Response.ErrorListener
) : super(method, url, errorListener) {
mListener = listener
this.mErrorListener = errorListener
}
@Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
return if (mHeaders != null) mHeaders!! else super.getHeaders()
}
override fun getBodyContentType(): String {
return "multipart/form-data;boundary=$boundary"
}
@Throws(AuthFailureError::class)
override fun getBody(): ByteArray? {
val bos = ByteArrayOutputStream()
val dos = DataOutputStream(bos)
try {
// populate text payload
val params = params
if (params != null && params.isNotEmpty()) {
textParse(dos, params, paramsEncoding)
}
// populate data byte payload
val data =
byteData
if (data != null && data.isNotEmpty()) {
dataParse(dos, data)
}
// close multipart form data after text and file data
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
return bos.toByteArray()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse?> {
return try {
Response.success(
response,
HttpHeaderParser.parseCacheHeaders(response)
)
} catch (e: Exception) {
Response.error(ParseError(e))
}
}
override fun deliverResponse(response: NetworkResponse?) {
mListener.onResponse(response)
}
override fun deliverError(error: VolleyError?) {
mErrorListener.onErrorResponse(error)
}
/**
* Parse string map into data output stream by key and value.
*
* @param dataOutputStream data output stream handle string parsing
* @param params string inputs collection
* @param encoding encode the inputs, default UTF-8
* @throws IOException
*/
@Throws(IOException::class)
private fun textParse(
dataOutputStream: DataOutputStream,
params: Map<String, String>,
encoding: String
) {
try {
for ((key, value) in params) {
buildTextPart(dataOutputStream, key, value)
}
} catch (uee: UnsupportedEncodingException) {
throw RuntimeException("Encoding not supported: $encoding", uee)
}
}
/**
* Parse data into data output stream.
*
* @param dataOutputStream data output stream handle file attachment
* @param data loop through data
* @throws IOException
*/
@Throws(IOException::class)
private fun dataParse(dataOutputStream: DataOutputStream, data: Map<String, DataPart>) {
for ((key, value) in data) {
buildDataPart(dataOutputStream, value, key)
}
}
/**
* Write string data into header and data output stream.
*
* @param dataOutputStream data output stream handle string parsing
* @param parameterName name of input
* @param parameterValue value of input
* @throws IOException
*/
@Throws(IOException::class)
private fun buildTextPart(
dataOutputStream: DataOutputStream,
parameterName: String,
parameterValue: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"$parameterName\"$lineEnd")
//dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
dataOutputStream.writeBytes(lineEnd)
dataOutputStream.writeBytes(parameterValue + lineEnd)
}
/**
* Write data file into header and data output stream.
*
* @param dataOutputStream data output stream handle data parsing
* @param dataFile data byte as DataPart from collection
* @param inputName name of data input
* @throws IOException
*/
@Throws(IOException::class)
private fun buildDataPart(
dataOutputStream: DataOutputStream,
dataFile: DataPart,
inputName: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes(
"Content-Disposition: form-data; name=\"" +
inputName + "\"; filename=\"" + dataFile.fileName + "\"" + lineEnd
)
if (dataFile.type != null && !dataFile.type!!.trim { it <= ' ' }.isEmpty()) {
dataOutputStream.writeBytes("Content-Type: " + dataFile.type + lineEnd)
}
dataOutputStream.writeBytes(lineEnd)
val fileInputStream = ByteArrayInputStream(dataFile.content)
var bytesAvailable: Int = fileInputStream.available()
val maxBufferSize = 1024 * 1024
var bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffer = ByteArray(bufferSize)
var bytesRead: Int = fileInputStream.read(buffer, 0, bufferSize)
while (bytesRead > 0) {
dataOutputStream.write(buffer, 0, bufferSize)
bytesAvailable = fileInputStream.available()
bufferSize = Math.min(bytesAvailable, maxBufferSize)
bytesRead = fileInputStream.read(buffer, 0, bufferSize)
}
dataOutputStream.writeBytes(lineEnd)
}
/**
* Simple data container use for passing byte file
*/
class DataPart {
var fileName: String? = null
var content: ByteArray? = null
var type: String? = null
/**
* Constructor with data.
*
* @param name label of data
* @param data byte data
*/
constructor(name: String?, data: ByteArray) {
fileName = name
content = data
}
/**
* Constructor with mime data type.
*
* @param name label of data
* @param data byte data
* @param mimeType mime data like "image/jpeg"
*/
constructor(name: String?, data: ByteArray, mimeType: String?) {
fileName = name
content = data
type = mimeType
}
}
}

@ -1,4 +1,4 @@
package net.pokeranalytics.android.calculus.calcul package net.pokeranalytics.android.calcul
import net.pokeranalytics.android.calculus.AggregationType import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph

@ -1,4 +1,4 @@
package net.pokeranalytics.android.calculus.calcul package net.pokeranalytics.android.calcul
import android.content.Context import android.content.Context
import com.github.mikephil.charting.data.* import com.github.mikephil.charting.data.*

@ -1,4 +1,4 @@
package net.pokeranalytics.android.calculus.calcul package net.pokeranalytics.android.calcul
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator

@ -1,4 +1,4 @@
package net.pokeranalytics.android.calculus.calcul package net.pokeranalytics.android.calcul
import android.content.Context import android.content.Context
import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarDataSet

@ -1,4 +1,4 @@
package net.pokeranalytics.android.calculus.calcul package net.pokeranalytics.android.calcul
import android.content.Context import android.content.Context
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -34,8 +34,8 @@ class StatRepresentable(var stat: Stat) : RowRepresentable {
Stat.HANDS_PLAYED -> R.string.number_of_hands Stat.HANDS_PLAYED -> R.string.number_of_hands
Stat.LOCATIONS_PLAYED -> R.string.locations_played Stat.LOCATIONS_PLAYED -> R.string.locations_played
Stat.LONGEST_STREAKS -> R.string.longest_streaks Stat.LONGEST_STREAKS -> R.string.longest_streaks
Stat.MAXIMUM_NET_RESULT -> R.string.max_net_result Stat.MAXIMUM_NETRESULT -> R.string.max_net_result
Stat.MINIMUM_NET_RESULT -> R.string.min_net_result Stat.MINIMUM_NETRESULT -> R.string.min_net_result
Stat.MAXIMUM_DURATION -> R.string.longest_session Stat.MAXIMUM_DURATION -> R.string.longest_session
Stat.DAYS_PLAYED -> R.string.days_played Stat.DAYS_PLAYED -> R.string.days_played
Stat.TOTAL_BUYIN -> R.string.total_buyin Stat.TOTAL_BUYIN -> R.string.total_buyin
@ -47,7 +47,7 @@ class StatRepresentable(var stat: Stat) : RowRepresentable {
} }
} }
override val resId: Int override val resId: Int?
get() { get() {
return resId(this.stat) return resId(this.stat)
} }

@ -9,7 +9,9 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.filter import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.util.extensions.startOfDay import net.pokeranalytics.android.util.extensions.startOfDay
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
@ -33,8 +35,7 @@ class Calculator {
var filterId: String? = null, var filterId: String? = null,
private var aggregationType: AggregationType? = null, private var aggregationType: AggregationType? = null,
var userGenerated: Boolean = false, var userGenerated: Boolean = false,
var reportSetupId: String? = null, var reportSetupId: String? = null
var includedTransactions: List<TransactionType> = listOf()
) { ) {
constructor( constructor(
@ -177,13 +178,13 @@ class Calculator {
val computableGroups: MutableList<ComputableGroup> = mutableListOf() val computableGroups: MutableList<ComputableGroup> = mutableListOf()
val combinations = options.criterias.combined() options.criterias.combined().forEach { comparatorQuery ->
// Timber.d("Combinations: ${ combinations.map { it.defaultName }}")
for (comparatorQuery in combinations) {
comparatorQuery.merge(options.query) comparatorQuery.merge(options.query)
val group = ComputableGroup(comparatorQuery) val group = ComputableGroup(comparatorQuery)
computableGroups.add(group) computableGroups.add(group)
} }
if (computableGroups.size == 0) { if (computableGroups.size == 0) {
@ -240,27 +241,14 @@ class Calculator {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues) val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val computables = computableGroup.computables(realm, options.shouldSortValues) val computables = computableGroup.computables(realm, options.shouldSortValues)
if (computables.size == 0) { // we don't want to return stats with 0 as a value when comparing best performances
return results
}
// Timber.d("#### Start computing group, ${computables.size} computables") // Timber.d("#### Start computing group, ${computables.size} computables")
results.addStat(NUMBER_OF_GAMES, computables.size.toDouble()) results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())
// computables.forEach { // computables.forEach {
// Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}") // Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}")
// } // }
var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble() val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
if (options.includedTransactions.isNotEmpty()) { results.addStat(NET_RESULT, sum)
for (transactionType in options.includedTransactions) {
val transactions = computableGroup.transactions(realm, transactionType, options.shouldSortValues)
val transactionRatedAmount = transactions.sum(Transaction.Field.RATED_AMOUNT.identifier).toDouble()
ratedNet += transactionRatedAmount
}
}
results.addStat(NET_RESULT, ratedNet)
val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble() val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
results.addStat(HANDS_PLAYED, totalHands) results.addStat(HANDS_PLAYED, totalHands)
@ -284,18 +272,18 @@ class Calculator {
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
maxNetResult?.let { maxNetResult?.let {
results.addStat(MAXIMUM_NET_RESULT, it) results.addStat(MAXIMUM_NETRESULT, it)
} }
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let { minNetResult?.let {
results.addStat(MINIMUM_NET_RESULT, it) results.addStat(MINIMUM_NETRESULT, it)
} }
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 -> Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStat(NET_BB_PER_100_HANDS, netBB100) results.addStat(NET_BB_PER_100_HANDS, netBB100)
} }
Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi -> Stat.returnOnInvestment(sum, totalBuyin)?.let { roi ->
results.addStat(ROI, roi) results.addStat(ROI, roi)
} }
@ -312,7 +300,7 @@ class Calculator {
var average = 0.0 // also used for standard deviation later var average = 0.0 // also used for standard deviation later
if (computables.size > 0) { if (computables.size > 0) {
average = ratedNet / computables.size.toDouble() average = sum / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble() val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble() val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble() val avgBuyin = totalBuyin / computables.size.toDouble()
@ -382,7 +370,6 @@ class Calculator {
results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session) results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session)
results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session) results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session)
results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session) results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
results.addEvolutionValue(tBBSum, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue( results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()), (tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO, stat = WIN_RATIO,
@ -433,9 +420,9 @@ class Calculator {
} }
} }
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() ||
|| options.progressValues != Options.ProgressValues.NONE options.progressValues != Options.ProgressValues.NONE ||
|| options.computeDaysPlayed options.computeDaysPlayed
// Session Set // Session Set
if (shouldIterateOverSets) { if (shouldIterateOverSets) {
@ -533,7 +520,7 @@ class Calculator {
var hourlyRate = 0.0 var hourlyRate = 0.0
if (gHourlyDuration != null) { if (gHourlyDuration != null) {
hourlyRate = ratedNet / gHourlyDuration hourlyRate = sum / gHourlyDuration
if (sessionSets.size > 0) { if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate) results.addStat(HOURLY_RATE, hourlyRate)
@ -619,10 +606,10 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) { if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet) this.initStatsWithSet(sessionSet)
} else { } else {
ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 } ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet } bbSum = filteredSessions.sumByDouble { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumOf { it.estimatedHands } estimatedHands = filteredSessions.sumByDouble { it.estimatedHands }
} }
} }
} }

@ -4,14 +4,15 @@ import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
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.* import net.pokeranalytics.android.model.realm.ComputableResult
import timber.log.Timber import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
/** /**
* A sessionGroup of computable items identified by a name * A sessionGroup of computable items identified by a name
*/ */
class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null) { class ComputableGroup(var query: Query, var displayedStats: List<Stat>? = null) {
/** /**
* A subgroup used to compute stat variation * A subgroup used to compute stat variation
@ -84,17 +85,6 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
return sets return sets
} }
/**
* Retrieves the transactions on the relative [realm] filtered with the provided [conditions]
*/
fun transactions(realm: Realm, transactionType: TransactionType, sorted: Boolean = false): RealmResults<Transaction> {
val query = this.query.copy()
query.add(QueryCondition.AnyTransactionType(transactionType))
val sortedField = if (sorted) "date" else null
Timber.d("query = ${query.defaultName}")
return Filter.queryOn(realm, query, sortedField)
}
/** /**
* Nullifies used Realm results * Nullifies used Realm results
*/ */

@ -31,23 +31,6 @@ class Report(var options: Calculator.Options) {
this._results.add(result) this._results.add(result)
} }
fun max(stat: Stat): ComputedResults? {
var computedResults: ComputedResults? = null
var count = 0
var max = Double.MIN_VALUE
for (cr in this._results) {
cr.computedStat(stat)?.value?.let { value ->
count += 1
if (value > max) {
computedResults = cr
max = value
}
}
}
return if (count >= 2) { computedResults } else { null }
}
} }

@ -1,338 +0,0 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import android.os.CountDownTimer
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
interface NewPerformanceListener {
fun newBestPerformanceHandler()
}
class ReportWhistleBlower(var context: Context) {
private var sessions: RealmResults<Session>? = null
private var results: RealmResults<Result>? = null
private var currentTask: ReportTask? = null
private val currentNotifications: MutableList<String> = mutableListOf() // Performance.id
private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
var paused: Boolean = false
private var timer: CountDownTimer? = null
init {
val realm = Realm.getDefaultInstance()
this.sessions = realm.where(Session::class.java).findAll()
this.sessions?.addChangeListener { _ ->
requestReportLaunch()
}
this.results = realm.where(Result::class.java).findAll()
this.results?.addChangeListener { _ ->
requestReportLaunch()
}
realm.close()
}
fun addListener(newPerformanceListener: NewPerformanceListener) {
this.listeners.add(newPerformanceListener)
}
fun removeListener(listener: NewPerformanceListener) {
this.listeners.remove(listener)
}
fun requestReportLaunch() {
// Timber.d(">>> Launch report")
if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return
}
this.timer?.cancel()
val launchStart = 100L
val timer = object: CountDownTimer(launchStart, launchStart) {
override fun onTick(p0: Long) { }
override fun onFinish() {
launchReportTask()
}
}
this.timer = timer
timer.start()
}
private fun launchReportTask() {
synchronized(this) {
this.currentTask?.cancel()
val reportTask = ReportTask(this, this.context)
this.currentTask = reportTask
reportTask.start()
}
}
/**
* Pauses the whistleblower, for example when importing data
*/
fun pause() {
this.paused = true
this.currentTask?.cancel()
this.currentTask = null
}
fun resume() {
this.paused = false
this.requestReportLaunch()
}
fun has(performanceId: String): Boolean {
return this.currentNotifications.contains(performanceId)
}
fun notify(performance: Performance) {
this.currentNotifications.add(performance.id)
for (listener in this.listeners) {
listener.newBestPerformanceHandler()
}
}
fun clearNotifications() {
this.currentNotifications.clear()
}
}
class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Context) {
private var cancelled = false
var handler: (() -> Unit)? = null
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() {
messages.add("Starting task...")
launchReports()
}
fun cancel() {
this.cancelled = true
}
var messages: MutableList<String> = mutableListOf()
private fun launchReports() {
CoroutineScope(coroutineContext).launch {
val realm = Realm.getDefaultInstance()
// Basic
for (basicReport in StaticReport.basicReports) {
if (cancelled) {
break
}
launchReport(realm, basicReport)
}
// CustomField
val customFields = realm.where(CustomField::class.java)
.equalTo("type", CustomField.Type.LIST.uniqueIdentifier).findAll()
for (customField in customFields) {
if (cancelled) {
break
}
launchReport(realm, StaticReport.CustomFieldList(customField))
}
realm.close()
}
}
private fun launchReport(realm: Realm, report: StaticReport) {
// Timber.d(">>> launch report = $report")
when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, report)
else -> launchDefaultReport(realm, report)
}
}
private fun launchDefaultReport(realm: Realm, report: StaticReport) {
val options = Calculator.Options(
stats = report.stats,
criterias = report.criteria
)
val result = Calculator.computeStats(realm, options = options)
analyseDefaultReport(realm, report, result)
}
private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.entries.forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive)
analyseOptimalDuration(realm, report, key, duration)
}
this.handler?.let { it() }
}
private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) {
messages.add("Analyse report $staticReport...")
val nameSeparator = " "
for (stat in result.options.stats) {
// Timber.d("analyse stat: $stat for report: $staticReport")
// Get current performance
var query = performancesQuery(realm, staticReport, stat)
val customField: CustomField? =
(staticReport as? StaticReport.CustomFieldList)?.customField
customField?.let {
query = query.equalTo("customFieldId", it.id)
}
val currentPerf = query.findFirst()
// Store if necessary, delete if necessary
val bestComputedResults = result.max(stat)
bestComputedResults?.let { computedResults ->
messages.add("found new perf...")
val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator)
Timber.d("Best computed = $performanceName, ${computedResults.computedStat(Stat.NET_RESULT)?.value}")
var storePerf = true
currentPerf?.let {
messages.add("has current perf...")
currentPerf.name?.let { name ->
if (computedResults.group.query.getName(this.context, nameSeparator) == name) {
storePerf = false
}
}
currentPerf.objectId?.let { objectId ->
if (computedResults.group.query.objectId == objectId) {
storePerf = false
}
}
if (storePerf) {
realm.executeTransaction {
currentPerf.name = performanceName
currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id
}
this.whistleBlower.notify(currentPerf)
}
}
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) {
val performance = Performance(
staticReport,
stat,
performanceName,
performanceQuery.objectId,
customField?.id,
null
)
realm.executeTransaction { it.copyToRealm(performance) }
this.whistleBlower.notify(performance)
}
} ?: run { // if there is no max but a now irrelevant Performance, we delete it
messages.add("deletes current perf if necessary: $currentPerf...")
// Timber.d("NO best computed value, current perf = $currentPerf ")
currentPerf?.let { perf ->
realm.executeTransaction {
// Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
perf.deleteFromRealm()
}
}
}
}
}
private fun analyseOptimalDuration(realm: Realm, staticReport: StaticReport, key: PerformanceKey, duration: Double?) {
val performance = performancesQuery(realm, staticReport, key).findFirst()
duration?.let {
var storePerf = true
val formattedDuration = (duration / 3600 / 1000).formattedHourlyDuration()
performance?.let { perf ->
if (perf.value == duration) {
storePerf = false
}
if (storePerf) {
realm.executeTransaction {
perf.name = formattedDuration
perf.value = duration
}
}
}
if (storePerf) {
val perf = Performance(staticReport, key, name = formattedDuration, value = duration)
realm.executeTransaction { it.copyToRealm(perf) }
this.whistleBlower.notify(perf)
}
} ?: run { // no duration
performance?.let { perf ->
realm.executeTransaction {
perf.deleteFromRealm() // delete if the perf exists
}
}
}
}
private fun performancesQuery(realm: Realm, staticReport: StaticReport, key: PerformanceKey): RealmQuery<Performance> {
return realm.where(Performance::class.java)
.equalTo("reportId", staticReport.uniqueIdentifier)
.equalTo("key", key.value)
}
}

@ -4,7 +4,6 @@ import android.content.Context
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.FormattingException import net.pokeranalytics.android.exceptions.FormattingException
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.PerformanceKey
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
@ -23,7 +22,7 @@ class StatFormattingException(message: String) : Exception(message)
/** /**
* An enum representing all the types of Session statistics * An enum representing all the types of Session statistics
*/ */
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable, PerformanceKey { enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable {
NET_RESULT(1), NET_RESULT(1),
BB_NET_RESULT(2), BB_NET_RESULT(2),
@ -45,8 +44,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED(18), HANDS_PLAYED(18),
LOCATIONS_PLAYED(19), LOCATIONS_PLAYED(19),
LONGEST_STREAKS(20), LONGEST_STREAKS(20),
MAXIMUM_NET_RESULT(21), MAXIMUM_NETRESULT(21),
MINIMUM_NET_RESULT(22), MINIMUM_NETRESULT(22),
MAXIMUM_DURATION(23), MAXIMUM_DURATION(23),
DAYS_PLAYED(24), DAYS_PLAYED(24),
WINNING_SESSION_COUNT(25), WINNING_SESSION_COUNT(25),
@ -103,8 +102,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
} }
override val value: Int = this.uniqueIdentifier
override val resId: Int? override val resId: Int?
get() { get() {
return when (this) { return when (this) {
@ -129,8 +126,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED -> R.string.number_of_hands HANDS_PLAYED -> R.string.number_of_hands
LOCATIONS_PLAYED -> R.string.locations_played LOCATIONS_PLAYED -> R.string.locations_played
LONGEST_STREAKS -> R.string.longest_streaks LONGEST_STREAKS -> R.string.longest_streaks
MAXIMUM_NET_RESULT -> R.string.max_net_result MAXIMUM_NETRESULT -> R.string.max_net_result
MINIMUM_NET_RESULT -> R.string.min_net_result MINIMUM_NETRESULT -> R.string.min_net_result
MAXIMUM_DURATION -> R.string.longest_session MAXIMUM_DURATION -> R.string.longest_session
DAYS_PLAYED -> R.string.days_played DAYS_PLAYED -> R.string.days_played
TOTAL_BUYIN -> R.string.total_buyin TOTAL_BUYIN -> R.string.total_buyin
@ -152,7 +149,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
when (this) { when (this) {
// Amounts + red/green // Amounts + red/green
NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NET_RESULT, MINIMUM_NET_RESULT -> { NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NETRESULT, MINIMUM_NETRESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.toCurrency(currency), color) return TextFormat(value.toCurrency(currency), color)
} }
@ -208,7 +205,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records NUMBER_OF_GAMES -> R.string.number_of_records
NET_RESULT, BB_NET_RESULT -> R.string.total NET_RESULT -> R.string.total
STANDARD_DEVIATION -> R.string.net_result STANDARD_DEVIATION -> R.string.net_result
STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_ STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses

@ -39,28 +39,10 @@ class BankrollCalculator {
initialValue += bankroll.initialValue * rate initialValue += bankroll.initialValue * rate
} }
if (setup.virtualBankroll) {
val sum = realm.where(Transaction::class.java)
.equalTo("bankroll.id", bankroll.id)
.notEqualTo("type.kind", TransactionType.Value.TRANSFER.uniqueIdentifier)
.sum("amount") // we remove transfer as they don't impact the overall bankroll
transactionNet += rate * sum.toDouble()
} else {
bankroll.transactions?.let { transactions -> bankroll.transactions?.let { transactions ->
val sum = transactions.sum("amount") val sum = transactions.sum("amount")
transactionNet += rate * sum.toDouble() transactionNet += rate * sum.toDouble()
} }
bankroll.destinationTransactions?.let { transactions ->
for (transaction in transactions) {
val transferRate = transaction.transferRate ?: 1.0
transactionNet += transferRate * transaction.amount * -1
}
}
}
} }
report.transactionsNet = transactionNet report.transactionsNet = transactionNet
@ -89,18 +71,20 @@ class BankrollCalculator {
this.computeRiskOfRuin(report, result) this.computeRiskOfRuin(report, result)
} else { } else {
val results = Filter.queryOn<Result>(realm, baseQuery) val results = Filter.queryOn<Result>(realm, baseQuery)
report.netResult = results.sum("net").toDouble() report.netResult = results.sum("net").toDouble()
} }
val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
report.transactionBuckets[depositType.id]?.let { bucket -> report.transactionBuckets[depositType.id]?.let { bucket ->
report.depositTotal = bucket.transactions.sumOf { it.amount } report.depositTotal = bucket.transactions.sumByDouble { it.amount }
} }
val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
report.transactionBuckets[withdrawalType.id]?.let { bucket -> report.transactionBuckets[withdrawalType.id]?.let { bucket ->
report.withdrawalTotal = bucket.transactions.sumOf { it.amount } report.withdrawalTotal = bucket.transactions.sumByDouble { it.amount }
} }
report.generateGraphPointsIfNecessary() report.generateGraphPointsIfNecessary()

@ -13,6 +13,7 @@ import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.DataSetFactory import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import java.util.* import java.util.*
import kotlin.collections.HashMap
/** /**
* This class holds the results from the BankrollCalculator computations * This class holds the results from the BankrollCalculator computations
@ -63,7 +64,6 @@ class BankrollReport(var setup: BankrollReportSetup) {
*/ */
private fun computeBankrollTotal() { private fun computeBankrollTotal() {
this.total = this.initial + this.netResult + this.transactionsNet this.total = this.initial + this.netResult + this.transactionsNet
// Timber.d("init = $initial, net = $netResult, trans = $transactionsNet")
} }
/** /**

@ -24,12 +24,9 @@ class CashGameOptimalDurationCalculator {
companion object { companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval = private const val bucketInterval = 4 // number of duration tests inside the bucket to find the best duration
4 // number of duration tests inside the bucket to find the best duration private const val minimumValidityCount = 10 // the number of sessions inside a bucket to start having a reasonable average
private const val minimumValidityCount = private const val intervalValidity = 3 // the minimum number of unit between the shortest & longest valid buckets
10 // the number of sessions inside a bucket to start having a reasonable average
private const val intervalValidity =
3 // the minimum number of unit between the shortest & longest valid buckets
private const val polynomialDegree = 7 // the degree of the computed polynomial private const val polynomialDegree = 7 // the degree of the computed polynomial
/*** /***
@ -42,13 +39,7 @@ class CashGameOptimalDurationCalculator {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val query = Query().add(QueryCondition.IsCash) // cash game val query = Query().add(QueryCondition.IsCash) // cash game
query.add( query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online
if (isLive) {
QueryCondition.IsLive
} else {
QueryCondition.IsOnline
}
) // live / online
query.add(QueryCondition.EndDateNotNull) // ended query.add(QueryCondition.EndDateNotNull) // ended
query.add(QueryCondition.BiggestBetNotNull) // has BB value query.add(QueryCondition.BiggestBetNotNull) // has BB value
@ -65,7 +56,7 @@ class CashGameOptimalDurationCalculator {
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,23 +67,22 @@ 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
} }
val options = Calculator.Options() val options = Calculator.Options()
options.query = query options.query = query
val report = Calculator.computeStats(realm, options) val report = Calculator.computeStats(realm, options)
val stdBB = val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
val p = polynomialRegression(sessions, stdBB) val p = polynomialRegression(sessions, stdBB)
@ -134,7 +124,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
} }
@ -147,10 +137,7 @@ class CashGameOptimalDurationCalculator {
return y return y
} }
private fun polynomialRegression( private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray {
sessions: List<Session>,
bbStandardDeviation: Double?
): DoubleArray {
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE val stdBB = bbStandardDeviation ?: Double.MAX_VALUE

@ -1,7 +1,5 @@
package net.pokeranalytics.android.exceptions package net.pokeranalytics.android.exceptions
import net.pokeranalytics.android.model.Criteria
class ModelException(message: String) : Exception(message) class ModelException(message: String) : Exception(message)
class FormattingException(message: String) : Exception(message) class FormattingException(message: String) : Exception(message)
class RowRepresentableEditDescriptorException(message: String) : Exception(message) class RowRepresentableEditDescriptorException(message: String) : Exception(message)
@ -17,11 +15,8 @@ sealed class PokerAnalyticsException(message: String) : Exception(message) {
// object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition") // object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition")
// object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized") // object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized")
// object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable") // object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
class QueryValueMapUnknown(message: String = "fieldName is missing"): PokerAnalyticsException(message) object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing")
class QueryTypeUnhandled(clazz: String) : object QueryTypeUnhandled: PokerAnalyticsException(message = "queryWith type not handled")
PokerAnalyticsException(message = "queryWith type not handled: $clazz")
class ComparisonCriteriaUnhandled(criteria: Criteria) :
PokerAnalyticsException(message = "Criteria type not handled: ${criteria.uniqueIdentifier}")
object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected") object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected")
object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null") object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null")
// data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "queryWith element '$filterElementRow' type is missing") // data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "queryWith element '$filterElementRow' type is missing")

@ -164,7 +164,6 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20) object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
val queries: List<Query> val queries: List<Query>
get() { get() {
@ -244,27 +243,12 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
val queries = when (this.customFieldType(realm)) { val queries = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >() CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >() CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this) else -> throw PokerAnalyticsException.QueryTypeUnhandled
} }
realm.close() realm.close()
queries queries
} }
is Duration -> { else -> throw PokerAnalyticsException.QueryTypeUnhandled
val hourlyQueries = (0..12).map { i ->
val more = QueryCondition.Duration(i * 60)
more.operator = QueryCondition.Operator.MORE_OR_EQUAL
val less = QueryCondition.Duration((i + 1) * 60)
less.operator = QueryCondition.Operator.LESS
Query(more, less)
}.toMutableList()
val moreThan12Hours = QueryCondition.Duration(12 * 60)
moreThan12Hours.operator = QueryCondition.Operator.MORE_OR_EQUAL
hourlyQueries.add(Query(moreThan12Hours))
hourlyQueries
}
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
} }
} }

@ -1,38 +0,0 @@
package net.pokeranalytics.android.model
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.PerformanceKey
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
enum class LiveOnline(override var uniqueIdentifier: Int) : PerformanceKey, IntIdentifiable {
LIVE(0),
ONLINE(1);
companion object : IntSearchable<LiveOnline> {
override fun valuesInternal(): Array<LiveOnline> {
return values()
}
}
override val resId: Int?
get() {
return when (this) {
LIVE -> R.string.live
ONLINE -> R.string.online
}
}
override val value: Int
get() {
return this.uniqueIdentifier
}
val isLive: Boolean
get() {
return (this == LIVE)
}
}

@ -2,7 +2,6 @@ package net.pokeranalytics.android.model.extensions
import android.content.Context import android.content.Context
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -120,7 +119,7 @@ fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
.addTag(this.id) .addTag(this.id)
.build() .build()
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work) WorkManager.getInstance(context).enqueue(work)
} }
@ -131,7 +130,7 @@ val AbstractList<Session>.hourlyDuration: Double
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration) val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval) intervals.update(interval)
} }
return intervals.sumOf { it.hourlyDuration } return intervals.sumByDouble { it.hourlyDuration }
} }
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) { class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {

@ -11,10 +11,6 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query { class Query {
constructor(query: Query) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) { constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
this.add(elements.filterNotNull()) this.add(elements.filterNotNull())
@ -56,10 +52,10 @@ class Query {
} }
} }
fun getName(context: Context, separator: String = " + "): String { fun getName(context: Context): String {
return when (this._conditions.size) { return when (this._conditions.size) {
0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction... 0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction...
else -> this._conditions.joinToString(separator) { it.getDisplayNameWithValues(context) } else -> this._conditions.joinToString(" + ") { it.getDisplayNameWithValues(context) }
} }
} }
@ -104,25 +100,4 @@ class Query {
return this return this
} }
fun copy(): Query {
return Query(this)
}
/*
Returns the first object Id of any QueryCondition
*/
val objectId: String?
get() {
for (c in this._conditions) {
when (c) {
is QueryCondition.QueryDataCondition<*> -> {
c.objectId?.let { return it }
}
else -> {}
}
}
return null
}
} }

@ -23,7 +23,6 @@ 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.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.*
@ -56,24 +55,16 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
// inline fun <reified T : QueryCondition> more(): T { inline fun <reified T : QueryCondition> more(): T {
// return newInstance(T::class).apply { this.operator = Operator.MORE } return newInstance(T::class).apply { this.operator = Operator.MORE }
// }
//
// inline fun <reified T : QueryCondition> less(): T {
// return newInstance(T::class).apply { this.operator = Operator.LESS }
// }
inline fun <reified T : QueryCondition> moreOrEqual(): T {
return newInstance(T::class).apply { this.operator = Operator.MORE_OR_EQUAL }
} }
inline fun <reified T : QueryCondition> lessOrEqual(): T { inline fun <reified T : QueryCondition> less(): T {
return newInstance(T::class).apply { this.operator = Operator.LESS_OR_EQUAL } return newInstance(T::class).apply { this.operator = Operator.LESS }
} }
inline fun <reified T : QueryCondition> moreEqualOrLessEqual(): ArrayList<T> { inline fun <reified T : QueryCondition> moreOrLess(): ArrayList<T> {
return arrayListOf(moreOrEqual(), lessOrEqual()) return arrayListOf(more(), less())
} }
fun <T : QueryCondition> valueOf(name: String): T { fun <T : QueryCondition> valueOf(name: String): T {
@ -89,14 +80,13 @@ sealed class QueryCondition : RowRepresentable {
TransactionType::class.java -> AnyTransactionType() TransactionType::class.java -> AnyTransactionType()
TournamentName::class.java -> AnyTournamentName() TournamentName::class.java -> AnyTournamentName()
TournamentFeature::class.java -> AllTournamentFeature() TournamentFeature::class.java -> AllTournamentFeature()
else -> throw PokerAnalyticsException.QueryTypeUnhandled((T::class.java).name) else -> throw PokerAnalyticsException.QueryTypeUnhandled
} }
} }
inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? { inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let { FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh()
val distincts = when (T::class) { val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING) String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
@ -114,9 +104,7 @@ sealed class QueryCondition : RowRepresentable {
ANY, ANY,
ALL, ALL,
MORE, MORE,
MORE_OR_EQUAL,
LESS, LESS,
LESS_OR_EQUAL,
EQUALS, EQUALS,
TRUE, TRUE,
NOTNULL NOTNULL
@ -128,7 +116,7 @@ sealed class QueryCondition : RowRepresentable {
val groupId: String val groupId: String
get() { get() {
return when (this.operator) { return when (this.operator) {
Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return "${this.operator.name.toLowerCase().capitalize()}$baseId" Operator.MORE, Operator.LESS -> return "${this.operator.name.toLowerCase().capitalize()}$baseId"
else -> this.baseId else -> this.baseId
} }
} }
@ -136,7 +124,7 @@ sealed class QueryCondition : RowRepresentable {
val id: List<String> val id: List<String>
get() { get() {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return listOf("$baseId+${this.operator.name}") Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}")
else -> {} else -> {}
} }
@ -337,11 +325,6 @@ sealed class QueryCondition : RowRepresentable {
return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
} }
val objectId: String?
get() {
return this.listOfValues.firstOrNull()
}
} }
interface DateTime { interface DateTime {
@ -554,19 +537,19 @@ sealed class QueryCondition : RowRepresentable {
} }
class StartedFromDate(date: Date) : DateQuery(date) { class StartedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE_OR_EQUAL override var operator = Operator.MORE
} }
class StartedToDate(date: Date) : DateQuery(date) { class StartedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS_OR_EQUAL override var operator = Operator.LESS
} }
class EndedFromDate(date: Date) : DateQuery(date) { class EndedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE_OR_EQUAL override var operator = Operator.MORE
} }
class EndedToDate(date: Date) : DateQuery(date) { class EndedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS_OR_EQUAL override var operator = Operator.LESS
} }
class AnyDayOfWeek : ListOfInt() { class AnyDayOfWeek : ListOfInt() {
@ -648,11 +631,11 @@ sealed class QueryCondition : RowRepresentable {
object BiggestBetNotNull : NotNullQueryCondition() object BiggestBetNotNull : NotNullQueryCondition()
class StartedFromTime(date: Date) : TimeQuery(date) { class StartedFromTime(date: Date) : TimeQuery(date) {
override var operator = Operator.MORE_OR_EQUAL override var operator = Operator.MORE
} }
class EndedToTime(date: Date) : TimeQuery(date) { class EndedToTime(date: Date) : TimeQuery(date) {
override var operator = Operator.LESS_OR_EQUAL override var operator = Operator.LESS
} }
interface CustomFieldRelated { interface CustomFieldRelated {
@ -748,16 +731,9 @@ sealed class QueryCondition : RowRepresentable {
): RealmQuery<T> { ): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java) val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
// if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// val className = T::class.java fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
// fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown("fieldName missing for $this, class = $className")
// }
if (fieldName == null) {
val className = T::class.java
Timber.w("Possible missing filter configuration for $this in class $className")
} }
fieldName ?: return realmQuery fieldName ?: return realmQuery
when (this) { when (this) {
@ -848,7 +824,6 @@ sealed class QueryCondition : RowRepresentable {
} }
return realmQuery return realmQuery
} }
else -> {}
} }
if (this is CustomFieldRelated) { if (this is CustomFieldRelated) {
@ -877,22 +852,12 @@ sealed class QueryCondition : RowRepresentable {
else -> realmQuery else -> realmQuery
} }
} }
Operator.MORE_OR_EQUAL -> { Operator.MORE -> {
when (this) { when (this) {
is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue.startOfDay()) is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue.startOfDay())
is Duration -> realmQuery.greaterThan(fieldName, netDuration)
is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is Duration -> realmQuery.greaterThanOrEqualTo(fieldName, netDuration)
is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
is ListOfInt -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is NetAmountLost -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * -1)
is ListOfDouble -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.MORE -> {
when (this) {
is Duration -> realmQuery.greaterThan(fieldName, netDuration)
is SingleInt -> realmQuery.greaterThan(fieldName, singleValue) is SingleInt -> realmQuery.greaterThan(fieldName, singleValue)
is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first())
is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1) is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1)
@ -900,30 +865,12 @@ sealed class QueryCondition : RowRepresentable {
else -> realmQuery else -> realmQuery
} }
} }
Operator.LESS_OR_EQUAL -> { Operator.LESS -> {
when (this) { when (this) {
is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue.endOfDay()) is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue.endOfDay())
is Duration -> realmQuery.lessThan(fieldName, netDuration)
is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is Duration -> realmQuery.lessThanOrEqualTo(fieldName, netDuration)
is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
is ListOfInt -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is NetAmountLost -> {
realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * -1)
realmQuery.lessThan(fieldName, 0.0)
}
is NetAmountWon -> {
realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
realmQuery.greaterThan(fieldName, 0.0)
}
is ListOfDouble -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.LESS -> {
when (this) {
is Duration -> realmQuery.lessThan(fieldName, netDuration)
is SingleInt -> realmQuery.lessThan(fieldName, singleValue) is SingleInt -> realmQuery.lessThan(fieldName, singleValue)
is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first())
is NetAmountLost -> { is NetAmountLost -> {
@ -980,8 +927,8 @@ sealed class QueryCondition : RowRepresentable {
is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.MORE_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS, Operator.LESS_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> RowViewType.TITLE_CHECK.ordinal else -> RowViewType.TITLE_CHECK.ordinal
} }
} }
@ -994,8 +941,8 @@ sealed class QueryCondition : RowRepresentable {
is PastDay -> BottomSheetType.EDIT_TEXT is PastDay -> BottomSheetType.EDIT_TEXT
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.MORE_OR_EQUAL -> BottomSheetType.EDIT_TEXT Operator.MORE -> BottomSheetType.EDIT_TEXT
Operator.LESS, Operator.LESS_OR_EQUAL -> BottomSheetType.EDIT_TEXT Operator.LESS -> BottomSheetType.EDIT_TEXT
else -> BottomSheetType.NONE else -> BottomSheetType.NONE
} }
} }
@ -1022,27 +969,27 @@ sealed class QueryCondition : RowRepresentable {
is IsWeekDay -> R.string.week_days is IsWeekDay -> R.string.week_days
is IsWeekEnd -> R.string.weekend is IsWeekEnd -> R.string.weekend
is PastDay -> R.string.period_in_days is PastDay -> R.string.period_in_days
// is TournamentNumberOfPlayer -> { is TournamentNumberOfPlayer -> {
// when (this.operator) { when (this.operator) {
// Operator.MORE -> R.string.minimum Operator.MORE -> R.string.minimum
// Operator.LESS -> R.string.maximum Operator.LESS -> R.string.maximum
// else -> null else -> null
// } }
// } }
// is NetAmountWon -> { is NetAmountWon -> {
// when (this.operator) { when (this.operator) {
// Operator.MORE -> R.string.won_amount_more_than Operator.MORE -> R.string.won_amount_more_than
// Operator.LESS -> R.string.won_amount_less_than Operator.LESS -> R.string.won_amount_less_than
// else -> null else -> null
// } }
// } }
// is NetAmountLost -> { is NetAmountLost -> {
// when (this.operator) { when (this.operator) {
// Operator.MORE -> R.string.lost_amount_more_than Operator.MORE -> R.string.lost_amount_more_than
// Operator.LESS -> R.string.lost_amount_less_than Operator.LESS -> R.string.lost_amount_less_than
// else -> null else -> null
// } }
// } }
is TournamentFinalPosition -> { is TournamentFinalPosition -> {
when (this.operator) { when (this.operator) {
Operator.MORE -> R.string.minimum Operator.MORE -> R.string.minimum
@ -1052,10 +999,8 @@ sealed class QueryCondition : RowRepresentable {
} }
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE_OR_EQUAL -> R.string.more_or_equal_sign Operator.MORE -> R.string.more_than
Operator.MORE -> R.string.more_sign Operator.LESS -> R.string.less_than
Operator.LESS_OR_EQUAL -> R.string.less_or_equal_sign
Operator.LESS -> R.string.less_sign
else -> null else -> null
} }
} }

@ -90,10 +90,7 @@ data class CodedStake(var stakes: String) : Comparable<CodedStake> {
fun formattedStakes(): String { fun formattedStakes(): String {
val components = arrayListOf<String>() val components = arrayListOf<String>()
this.formattedBlinds()?.let { components.add(it) } this.formattedBlinds()?.let { components.add(it) }
if ((this.ante ?: -1.0) > 0.0) {
this.formattedAnte()?.let { components.add("($it)") } this.formattedAnte()?.let { components.add("($it)") }
}
return if (components.isNotEmpty()) { return if (components.isNotEmpty()) {
components.joinToString(" ") components.joinToString(" ")

@ -2,8 +2,6 @@ package net.pokeranalytics.android.model.migrations
import android.content.Context import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.where
import net.pokeranalytics.android.PokerAnalyticsApplication
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.* import net.pokeranalytics.android.model.realm.*
@ -18,9 +16,7 @@ class Patcher {
companion object { companion object {
fun patchAll(application: PokerAnalyticsApplication) { fun patchAll(context: Context) {
val context = application.applicationContext
// NOTE: it's more than possible that at one point many patches become redundant // NOTE: it's more than possible that at one point many patches become redundant
// with each other // with each other
@ -42,39 +38,52 @@ class Patcher {
Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) { Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) {
patchStakes() patchStakes()
} }
Preferences.executeOnce(Preferences.Keys.PATCH_NEGATIVE_LIMITS, context) {
patchNegativeLimits()
}
Preferences.executeOnce(Preferences.Keys.CLEAN_BLINDS_FILTERS, context) {
cleanBlindsFilters()
}
Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) { Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
patchMissingTransactionTypes(context)
}
Preferences.executeOnce(Preferences.Keys.PATCH_ZERO_TABLE, context) { val realm = Realm.getDefaultInstance()
patchZeroTable()
}
Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) { val lockedTypes =
patchRatedAmounts() realm.where(TransactionType::class.java).equalTo("lock", true).findAll()
if (lockedTypes.size == 3) {
Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
val newTypes = arrayOf(
TransactionType.Value.STACKING_INCOMING,
TransactionType.Value.STACKING_OUTGOING
)
realm.executeTransaction {
Seed.createDefaultTransactionTypes(newTypes, context, realm)
}
}
} }
patchPerformances(application) realm.close()
}
} }
private fun patchMissingTransactionTypes(context: Context) { private fun patchMissingTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val transactionTypes = TransactionType.Value.values() val depositType = TransactionType.Value.DEPOSIT
val deposit = realm.where(TransactionType::class.java)
.equalTo("kind", depositType.uniqueIdentifier).findFirst()
if (deposit == null) {
realm.executeTransaction { realm.executeTransaction {
Seed.createDefaultTransactionTypes(transactionTypes, context, realm) Seed.createDefaultTransactionTypes(arrayOf(depositType), context, realm)
}
} }
val withdrawalType = TransactionType.Value.WITHDRAWAL
val withdrawal = realm.where(TransactionType::class.java)
.equalTo("kind", withdrawalType.uniqueIdentifier).findFirst()
if (withdrawal == null) {
realm.executeTransaction {
Seed.createDefaultTransactionTypes(arrayOf(withdrawalType), context, realm)
}
}
realm.close() realm.close()
} }
private fun patchBreaks() { private fun patchBreaks() {
@ -141,28 +150,6 @@ class Patcher {
realm.close() realm.close()
} }
private fun patchNegativeLimits() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).lessThan("limit", 0).findAll()
sessions.forEach { session ->
session.limit = null
}
}
realm.close()
}
private fun cleanBlindsFilters() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val blindFilterConditions = realm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll()
val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id }
val filters = realm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll()
filters.deleteAllFromRealm()
}
realm.close()
}
/* /*
02/09/19: A bug with the session set management made them kept instead of deleted, 02/09/19: A bug with the session set management made them kept instead of deleted,
thus making duration calculation wrong thus making duration calculation wrong
@ -195,40 +182,6 @@ class Patcher {
} }
realm.close() realm.close()
} }
private fun patchPerformances(application: PokerAnalyticsApplication) {
val realm = Realm.getDefaultInstance()
val sessionCount = realm.where<Session>().findAll().size
val performanceCount = realm.where<Performance>().findAll().size
if (sessionCount > 1 && performanceCount == 0) {
application.reportWhistleBlower?.requestReportLaunch()
}
realm.close()
}
private fun patchZeroTable() {
val realm = Realm.getDefaultInstance()
val zero = 0
val sessions = realm.where<Session>().equalTo("numberOfTables", zero).findAll()
realm.executeTransaction {
sessions.forEach { s ->
s.numberOfTables = 1
}
}
realm.close()
}
private fun patchRatedAmounts() {
val realm = Realm.getDefaultInstance()
val transactions = realm.where<Transaction>().findAll()
realm.executeTransaction {
transactions.forEach { t ->
t.computeRatedAmount()
}
}
realm.close()
} }
} }
}

@ -21,8 +21,7 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 0) { if (currentVersion == 0) {
Timber.d("*** Running migration 1") Timber.d("*** Running migration 1")
schema.get("Filter")?.addField("entityType", Int::class.java) schema.get("Filter")?.addField("entityType", Int::class.java)?.setNullable("entityType", true)
?.setNullable("entityType", true)
schema.get("FilterElement")?.let { schema.get("FilterElement")?.let {
it.setNullable("filterName", true) it.setNullable("filterName", true)
it.setNullable("sectionName", true) it.setNullable("sectionName", true)
@ -84,8 +83,7 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 3) { if (currentVersion == 3) {
Timber.d("*** Running migration ${currentVersion + 1}") Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Result")?.addField("numberOfRebuy", Double::class.java) schema.get("Result")?.addField("numberOfRebuy", Double::class.java)?.setNullable("numberOfRebuy", true)
?.setNullable("numberOfRebuy", true)
currentVersion++ currentVersion++
} }
@ -119,8 +117,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("CustomField")?.let { schema.get("CustomField")?.let {
it.addField("type", Integer::class.java).setNullable("type", false) it.addField("type", Integer::class.java).setNullable("type", false)
it.addField("duplicateValue", Boolean::class.java) it.addField("duplicateValue", Boolean::class.java)
it.addField("sortCondition", Integer::class.java) it.addField("sortCondition", Integer::class.java).setRequired("sortCondition", true)
.setRequired("sortCondition", true)
it.addRealmListField("entries", customFieldEntrySchema) it.addRealmListField("entries", customFieldEntrySchema)
} }
@ -137,8 +134,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("ReportSetup")?.let { schema.get("ReportSetup")?.let {
it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true) it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true)
it.addRealmListField("criteriaCustomFieldIds", String::class.java) it.addRealmListField("criteriaCustomFieldIds", String::class.java)
it.addRealmListField("criteriaIds", Int::class.java) it.addRealmListField("criteriaIds", Int::class.java).setNullable("criteriaIds", true)
.setNullable("criteriaIds", true)
it.removeField("filters") it.removeField("filters")
schema.get("Filter")?.let { filterSchema -> schema.get("Filter")?.let { filterSchema ->
it.addRealmObjectField("filter", filterSchema) it.addRealmObjectField("filter", filterSchema)
@ -199,8 +195,7 @@ class PokerAnalyticsMigration : RealmMigration {
val cardSchema = schema.create("Card") val cardSchema = schema.create("Card")
cardSchema.addField("value", Int::class.java).setRequired("value", false) cardSchema.addField("value", Int::class.java).setRequired("value", false)
cardSchema.addField("suitIdentifier", Int::class.java) cardSchema.addField("suitIdentifier", Int::class.java).setRequired("suitIdentifier", false)
.setRequired("suitIdentifier", false)
cardSchema.addField("index", Int::class.java) cardSchema.addField("index", Int::class.java)
hhSchema.addRealmListField("board", cardSchema) hhSchema.addRealmListField("board", cardSchema)
@ -208,12 +203,10 @@ class PokerAnalyticsMigration : RealmMigration {
actionSchema.addField("streetIdentifier", Int::class.java) actionSchema.addField("streetIdentifier", Int::class.java)
actionSchema.addField("index", Int::class.java) actionSchema.addField("index", Int::class.java)
actionSchema.addField("position", Int::class.java) actionSchema.addField("position", Int::class.java)
actionSchema.addField("typeIdentifier", Int::class.java) actionSchema.addField("typeIdentifier", Int::class.java).setRequired("typeIdentifier", false)
.setRequired("typeIdentifier", false)
actionSchema.addField("amount", Double::class.java).setRequired("amount", false) actionSchema.addField("amount", Double::class.java).setRequired("amount", false)
actionSchema.addField("effectiveAmount", Double::class.java) actionSchema.addField("effectiveAmount", Double::class.java)
actionSchema.addField("positionRemainingStack", Double::class.java) actionSchema.addField("positionRemainingStack", Double::class.java).setRequired("positionRemainingStack", false)
.setRequired("positionRemainingStack", false)
hhSchema.addRealmListField("actions", actionSchema) hhSchema.addRealmListField("actions", actionSchema)
val playerSetupSchema = schema.create("PlayerSetup") val playerSetupSchema = schema.create("PlayerSetup")
@ -289,52 +282,6 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++ currentVersion++
} }
// Migrate to version 13
if (currentVersion == 12) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("transferRate", Double::class.java)
.setNullable("transferRate", true)
schema.get("Bankroll")?.let { bs ->
ts.addRealmObjectField("destination", bs)
} ?: throw PAIllegalStateException("Bankroll schema not found")
}
schema.create("Performance")?.let { ps ->
ps.addField("id", String::class.java).setRequired("id", true)
ps.addPrimaryKey("id")
ps.addField("reportId", Int::class.java)
ps.addField("key", Int::class.java)
ps.addField("name", String::class.java)
ps.addField("objectId", String::class.java)//.setNullable("objectId", true)
ps.addField("customFieldId", String::class.java)//.setNullable("customFieldId", true)
ps.addField("value", Double::class.java).setRequired("value", false) //.setNullable("value", true)
}
currentVersion++
}
// Migrate to version 14
if (currentVersion == 13) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("ratedAmount", Double::class.java)
} ?: throw PAIllegalStateException("Transaction schema not found")
//transactionTypeIds
schema.get("UserConfig")?.let { ucs ->
ucs.addField("transactionTypeIds", String::class.java).setRequired("transactionTypeIds", true)
} ?: throw PAIllegalStateException("UserConfig schema not found")
schema.get("Performance")?.let { ps ->
if (!ps.isPrimaryKey("id")) {
ps.addPrimaryKey("id")
}
}
currentVersion++
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -60,12 +60,6 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
@LinkingObjects("bankroll") @LinkingObjects("bankroll")
val transactions: RealmResults<Transaction>? = null val transactions: RealmResults<Transaction>? = null
/**
* The list of transactions where the bankroll is the destination
*/
@LinkingObjects("destination")
val destinationTransactions: RealmResults<Transaction>? = null
// The currency of the bankroll // The currency of the bankroll
var currency: Currency? = null var currency: Currency? = null

@ -2,12 +2,11 @@ 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.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class FilterCondition() : RealmObject() { open class FilterCondition() : RealmObject() {
@ -29,7 +28,6 @@ open class FilterCondition() : RealmObject() {
is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues }) is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues })
is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues }) is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues })
is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues }) is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues })
else -> {}
} }
} }
@ -51,9 +49,6 @@ open class FilterCondition() : RealmObject() {
var stringValue: String? = null var stringValue: String? = null
var operator: Int? = null var operator: Int? = null
@LinkingObjects("filterConditions")
val filters: RealmResults<Filter>? = null
inline fun <reified T> getValues(): ArrayList <T> { inline fun <reified T> getValues(): ArrayList <T> {
return when (T::class) { return when (T::class) {
Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } } Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } }

@ -1,72 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.lookupForNameInAllTablesById
import java.util.*
interface PerformanceKey {
val resId: Int?
val value: Int
}
open class Performance() : RealmObject() {
@PrimaryKey
var id: String = UUID.randomUUID().toString()
constructor(
report: StaticReport,
key: PerformanceKey,
name: String? = null,
objectId: String? = null,
customFieldId: String? = null,
value: Double? = null
) : this() {
this.reportId = report.uniqueIdentifier
this.key = key.value
this.name = name
this.objectId = objectId
this.customFieldId = customFieldId
this.value = value
}
var reportId: Int = 0
var key: Int = 0
var name: String? = null
var objectId: String? = null
var customFieldId: String? = null
var value: Double? = null
fun toStaticReport(realm: Realm): StaticReport {
return StaticReport.newInstance(realm, this.reportId, this.customFieldId)
}
fun displayValue(realm: Realm): CharSequence {
this.name?.let { return it }
this.objectId?.let { realm.lookupForNameInAllTablesById(it) }
return NULL_TEXT
}
val stat: Stat
get() {
return Stat.valueByIdentifier(this.key.toInt())
}
val resId: Int?
get() {
return when (this.reportId) {
StaticReport.OptimalDuration.uniqueIdentifier -> LiveOnline.valueByIdentifier(this.key).resId
else -> stat.resId
}
}
}

@ -4,20 +4,27 @@ import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
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.RowUpdatable 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.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.RANDOM_PLAYER import net.pokeranalytics.android.util.RANDOM_PLAYER
import net.pokeranalytics.android.util.extensions.isSameDay
import net.pokeranalytics.android.util.extensions.mediumDate
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable { open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -37,6 +44,13 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
@Ignore @Ignore
override val viewType: Int = RowViewType.ROW_PLAYER.ordinal override val viewType: Int = RowViewType.ROW_PLAYER.ordinal
@Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf()
@Ignore
private var commentsToDelete: ArrayList<Comment> = ArrayList()
override fun isValidForDelete(realm: Realm): Boolean { override fun isValidForDelete(realm: Realm): Boolean {
//TODO //TODO
return true return true
@ -55,16 +69,70 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
return R.string.relationship_error return R.string.relationship_error
} }
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.name return this.name
} }
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
PlayerPropertiesRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
PlayerPropertiesRow.NAME -> this.name = value as String? ?: "" PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: "" PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerPropertiesRow.IMAGE -> this.picture = value as? String PlayerPropertiesRow.IMAGE -> this.picture = value as String? ?: ""
}
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(PlayerPropertiesRow.IMAGE)
rows.add(PlayerPropertiesRow.NAME)
rows.add(PlayerPropertiesRow.SUMMARY)
if (comments.size > 0) {
// Adds Comments section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, R.string.comments))
val currentCommentCalendar = Calendar.getInstance()
val currentDateCalendar = Calendar.getInstance()
val commentsToDisplay = ArrayList<Comment>()
commentsToDisplay.addAll(comments)
commentsToDisplay.sortByDescending { it.date }
commentsToDisplay.forEachIndexed { index, comment ->
currentCommentCalendar.time = comment.date
if (!currentCommentCalendar.isSameDay(currentDateCalendar) || index == 0) {
currentDateCalendar.time = currentCommentCalendar.time
// Adds day sub section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_SUBTITLE, title = currentDateCalendar.time.mediumDate()))
} }
// Adds comment
rows.add(comment)
}
rows.add(SeparatorRow())
}
return rows
} }
/** /**
@ -74,6 +142,57 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
return picture != null && picture?.isNotEmpty() == true return picture != null && picture?.isNotEmpty() == true
} }
/**
* Update row representation
*/
fun updateRowRepresentation() {
this.rowRepresentation = this.updatedRowRepresentationForCurrentState()
}
/**
* Add an entry
*/
fun addComment(): Comment {
val entry = Comment()
this.comments.add(entry)
updateRowRepresentation()
return entry
}
/**
* Delete an entry
*/
fun deleteComment(comment: Comment) {
commentsToDelete.add(comment)
this.comments.remove(comment)
updateRowRepresentation()
}
/**
* Clean up deleted entries
*/
fun cleanupComments() { // called when saving the custom field
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
this.commentsToDelete.forEach { // entries are out of realm
realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
}
}
realm.close()
this.commentsToDelete.clear()
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
when (row) {
PlayerPropertiesRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name))
PlayerPropertiesRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.summary))
}
return null
}
val initials: String val initials: String
get() { get() {
return if (this.name.isNotEmpty()) { return if (this.name.isNotEmpty()) {
@ -94,8 +213,4 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
} }
} }
fun hands(realm: Realm): RealmResults<HandHistory> {
return realm.where(HandHistory::class.java).equalTo("playerSetups.player.id", this.id).findAll()
}
} }

@ -6,7 +6,7 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria

@ -105,7 +105,7 @@ open class Result : RealmObject(), Filterable {
val isPositive: Int val isPositive: Int
get() { get() {
return if (session?.isTournament() == true) { return if (session?.isTournament() == true) {
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
} }
@ -114,7 +114,7 @@ open class Result : RealmObject(), Filterable {
// Computes the Net // Computes the Net
private fun computeNet(withBuyin: Boolean? = null) { private fun computeNet(withBuyin: Boolean? = null) {
val transactionsSum = transactions.sumOf { it.amount } val transactionsSum = transactions.sumByDouble { it.amount }
// choose the method to compute the net // choose the method to compute the net
var useBuyin = withBuyin ?: true var useBuyin = withBuyin ?: true

@ -274,13 +274,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The limit type: NL, PL... // The limit type: NL, PL...
var limit: Int? = null var limit: Int? = null
set(value) {
field = if (value != null && value >= 0) {
value
} else {
null
}
}
// The game played during the Session // The game played during the Session
var game: Game? = null var game: Game? = null
@ -294,11 +287,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The number of tables played at the same time // The number of tables played at the same time
var numberOfTables: Int = 1 var numberOfTables: Int = 1
set(value) { set(value) {
if (value > 0) {
field = value field = value
this.computeStats() this.computeStats()
} }
}
// The hand histories of the session // The hand histories of the session
@LinkingObjects("session") @LinkingObjects("session")
@ -676,8 +667,41 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
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
@ -686,8 +710,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
*/ */
fun delete() { fun delete() {
CrashLogging.log("Deletes session. Id = ${this.id}")
if (isValid) { if (isValid) {
// CrashLogging.log("Deletes session. Id = ${this.id}")
realm.executeTransaction { realm.executeTransaction {
cleanup() cleanup()
deleteFromRealm() deleteFromRealm()
@ -718,9 +742,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
copy.game = this.game copy.game = this.game
copy.limit = this.limit copy.limit = this.limit
copy.cgBlinds = this.cgBlinds copy.cgBiggestBet = this.cgBiggestBet
copy.cgAnte = this.cgAnte copy.cgAnte = this.cgAnte
copy.location = this.location
copy.tournamentEntryFee = this.tournamentEntryFee copy.tournamentEntryFee = this.tournamentEntryFee
copy.tournamentFeatures = this.tournamentFeatures copy.tournamentFeatures = this.tournamentFeatures
copy.tournamentName = this.tournamentName copy.tournamentName = this.tournamentName
@ -743,12 +766,32 @@ 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
@ -759,6 +802,7 @@ 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()
@ -897,7 +941,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.bbNet, this.bbNet,
this.estimatedHands this.estimatedHands
) )
Stat.AVERAGE_NET_BB, Stat.BB_NET_RESULT -> this.bbNet Stat.AVERAGE_NET_BB -> this.bbNet
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble() Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
Stat.HANDS_PLAYED -> this.estimatedHands Stat.HANDS_PLAYED -> this.estimatedHands
@ -1032,6 +1076,33 @@ 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) {

@ -61,9 +61,9 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override var netDuration: Long = 0L override var netDuration: Long = 0L
fun computeStats() { fun computeStats() {
this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0 this.ratedNet = this.sessions?.sumByDouble { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0 this.estimatedHands = this.sessions?.sumByDouble { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0 this.bbNet = this.sessions?.sumByDouble { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L
} }

@ -18,14 +18,15 @@ import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.collections.ArrayList
open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageable, StaticRowRepresentableDataSource, TimeFilterable, open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageable, StaticRowRepresentableDataSource, TimeFilterable,
Filterable, DatedBankrollGraphEntry { Filterable, DatedBankrollGraphEntry {
companion object { companion object {
fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double, comment: String? = null, destination: Bankroll? = null, transferRate: Double? = null): Transaction { fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double, comment: String? = null): Transaction {
val transaction = realm.copyToRealm(Transaction()) val transaction = realm.copyToRealm(Transaction())
transaction.date = date ?: Date() transaction.date = date ?: Date()
@ -33,14 +34,14 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
transaction.type = type transaction.type = type
transaction.bankroll = bankroll transaction.bankroll = bankroll
transaction.comment = comment ?: "" transaction.comment = comment ?: ""
transaction.destination = destination
transaction.transferRate = transferRate
if (destination != null) { // we make sure transfers are negative return transaction
transaction.amount = abs(amount) * -1
} }
return transaction val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(TransactionPropertiesRow.values())
rows
} }
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? { fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
@ -73,13 +74,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
// The amount of the transaction // The amount of the transaction
override var amount: Double = 0.0 override var amount: Double = 0.0
set(value) {
field = value
computeRatedAmount()
}
// The amount of the transaction
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()
@ -94,12 +88,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
// A user comment // A user comment
var comment: String = "" var comment: String = ""
// The destination Bankroll of a transfer
var destination: Bankroll? = null
// The rate of the transfer when bankrolls are in different currencies
var transferRate: Double? = null
// Timed interface // Timed interface
override var dayOfWeek: Int? = null override var dayOfWeek: Int? = null
override var month: Int? = null override var month: Int? = null
@ -109,20 +97,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
@Ignore @Ignore
override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal
enum class Field(val identifier: String) {
RATED_AMOUNT("ratedAmount")
}
fun computeRatedAmount() {
val rate = this.bankroll?.currency?.rate ?: 1.0
this.ratedAmount = rate * this.amount
}
val displayAmount: Double
get() { // for transfers we want to show a positive value (in the feed for instance)
return if (this.destination == null) { this.amount } else { abs(this.amount) }
}
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
TransactionPropertiesRow.BANKROLL -> bankroll = value as Bankroll? TransactionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
@ -130,13 +104,11 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
TransactionPropertiesRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble() TransactionPropertiesRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionPropertiesRow.COMMENT -> comment = value as String? ?: "" TransactionPropertiesRow.COMMENT -> comment = value as String? ?: ""
TransactionPropertiesRow.DATE -> date = value as Date? ?: Date() TransactionPropertiesRow.DATE -> date = value as Date? ?: Date()
TransactionPropertiesRow.DESTINATION -> destination = value as? Bankroll
TransactionPropertiesRow.RATE -> transferRate = (value as String?)?.toDouble()
} }
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation() return rowRepresentation
} }
override fun isValidForSave(): Boolean { override fun isValidForSave(): Boolean {
@ -172,24 +144,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
return SaveValidityStatus.VALID return SaveValidityStatus.VALID
} }
fun rowRepresentation(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
when (this.type?.kind) {
TransactionType.Value.TRANSFER.uniqueIdentifier -> {
if (this.bankroll != null && this.bankroll?.currency?.code != this.destination?.currency?.code) {
rows.addAll(TransactionPropertiesRow.transferRateRows)
} else {
rows.addAll(TransactionPropertiesRow.transferRows)
}
}
else -> {
rows.addAll(TransactionPropertiesRow.baseRows)
}
}
return rows
}
// GraphIdentifiableEntry // GraphIdentifiableEntry
@Ignore @Ignore

@ -19,6 +19,7 @@ import net.pokeranalytics.android.ui.view.rows.TransactionTypePropertiesRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable import net.pokeranalytics.android.util.enumerations.IntSearchable
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource,
@ -30,9 +31,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
DEPOSIT(1, true), DEPOSIT(1, true),
BONUS(2, true), BONUS(2, true),
STACKING_INCOMING(3, true), STACKING_INCOMING(3, true),
STACKING_OUTGOING(4, false), STACKING_OUTGOING(4, false);
TRANSFER(5, false),
EXPENSE(6, false); // not created by default, only used for poker base import atm
companion object : IntSearchable<Value> { companion object : IntSearchable<Value> {
@ -49,8 +48,6 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
BONUS -> R.string.bonus BONUS -> R.string.bonus
STACKING_INCOMING -> R.string.stacking_incoming STACKING_INCOMING -> R.string.stacking_incoming
STACKING_OUTGOING -> R.string.stacking_outgoing STACKING_OUTGOING -> R.string.stacking_outgoing
TRANSFER -> R.string.transfer
EXPENSE -> R.string.expense
} }
} }
@ -72,10 +69,6 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
throw PAIllegalStateException("Transaction type ${value.name} should exist in database!") throw PAIllegalStateException("Transaction type ${value.name} should exist in database!")
} }
fun getOrCreate(realm: Realm, value: Value, context: Context): TransactionType {
return getOrCreate(realm, value.localizedTitle(context), value.additive)
}
fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType { fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst() val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst()
return if (type != null) { return if (type != null) {

@ -3,8 +3,6 @@ package net.pokeranalytics.android.model.realm
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById
import java.util.* import java.util.*
open class UserConfig : RealmObject() { open class UserConfig : RealmObject() {
@ -27,15 +25,4 @@ open class UserConfig : RealmObject() {
var onlineDealtHandsPerHour: Int = 500 var onlineDealtHandsPerHour: Int = 500
var transactionTypeIds: String = ""
fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) {
this.transactionTypeIds = transactionTypes.joinToString(UUID_SEPARATOR) { it.id }
}
fun transactionTypes(realm: Realm): List<TransactionType> {
val ids = this.transactionTypeIds.split(UUID_SEPARATOR)
return ids.mapNotNull { realm.findById(it) }
}
} }

@ -190,11 +190,9 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/*** /***
* Configures a hand history with a [handSetup] * Configures a hand history with a [handSetup]
*/ */
fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) { fun configure(handSetup: HandSetup) {
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups) this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it } handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it } handSetup.ante?.let { this.ante = it }
@ -334,7 +332,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val sortedActions = this.sortedActions val sortedActions = this.sortedActions
val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index
?: sortedActions.size ?: sortedActions.size
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumOf { it.effectiveAmount } return this.anteSum + sortedActions.take(firstIndexOfStreet).sumByDouble { it.effectiveAmount }
} }
@Ignore @Ignore
@ -479,12 +477,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean? val heroWins: Boolean?
get() { get() {
return this.heroIndex?.let { heroIndex -> return this.heroIndex?.let { heroIndex ->
this.largestWonPot?.let { pot -> this.winnerPots.any { it.position == heroIndex }
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
} ?: run { } ?: run {
null null
} }
@ -586,14 +579,14 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position } val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position }
if (allinPositions.isEmpty()) { if (allinPositions.isEmpty()) {
runningPotAmount += streetActions.sumOf { it.effectiveAmount } runningPotAmount += streetActions.sumByDouble { it.effectiveAmount }
} else { } else {
val amountsPerPosition = mutableListOf<PositionAmount>() val amountsPerPosition = mutableListOf<PositionAmount>()
// get all committed amounts for the street by player, by allin // get all committed amounts for the street by player, by allin
this.positionIndexes.map { position -> this.positionIndexes.map { position ->
val playerActions = streetActions.filter { it.position == position } val playerActions = streetActions.filter { it.position == position }
val sum = playerActions.sumOf { it.effectiveAmount } val sum = playerActions.sumByDouble { it.effectiveAmount }
amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position))) amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position)))
} }
amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence
@ -646,8 +639,6 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pots.forEach { pot -> pots.forEach { pot ->
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList()) val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners // Distributes the pot for each winners
@ -663,7 +654,6 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
wp.amount += share wp.amount += share
} }
} }
}
} }
return wonPots.values return wonPots.values
@ -743,7 +733,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return boardHasWildCard || playerCardHasWildCard return boardHasWildCard || playerCardHasWildCard
} }
private val allFullCards: List<Card> val allFullCards: List<Card>
get() { get() {
val cards = mutableListOf<Card>() val cards = mutableListOf<Card>()
cards.addAll(this.board) cards.addAll(this.board)
@ -763,13 +753,4 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return cards.filter { it.isSuitWildCard } return cards.filter { it.isSuitWildCard }
} }
private val largestWonPot: WonPot?
get() {
return if (this.winnerPots.isNotEmpty()) { // needed, otherwise maxBy crashes
this.winnerPots.maxBy { it.amount }
} else {
null
}
}
} }

@ -17,8 +17,6 @@ class Seed(var context:Context) : Realm.Transaction {
fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) { fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) {
values.forEach { value -> values.forEach { value ->
if (value != TransactionType.Value.EXPENSE) {
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll() val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) { if (existing.isEmpty()) {
val type = TransactionType() val type = TransactionType()
@ -29,8 +27,6 @@ class Seed(var context:Context) : Realm.Transaction {
realm.insertOrUpdate(type) realm.insertOrUpdate(type)
} }
} }
}
} }
} }

@ -1,99 +0,0 @@
package net.pokeranalytics.android.ui.activity
import android.Manifest
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.activity.components.ResultCode
import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.util.copyStreamToFile
import timber.log.Timber
import java.io.File
class DatabaseCopyActivity : BaseActivity() {
private lateinit var fileURI: Uri
enum class IntentKey(val keyName: String) {
URI("uri")
}
companion object {
/**
* Create a new instance for result
*/
fun newInstanceForResult(context: FragmentActivity, uri: Uri) {
context.startActivityForResult(getIntent(context, uri), RequestCode.IMPORT.value)
}
private fun getIntent(context: Context, uri: Uri): Intent {
Timber.d("getIntent")
val intent = Intent(context, DatabaseCopyActivity::class.java)
intent.putExtra(IntentKey.URI.keyName, uri)
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("onCreate")
intent?.data?.let {
this.fileURI = it
} ?: run {
this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) ?: throw PAIllegalStateException("Uri not found")
}
// setContentView(R.layout.activity_import)
requestImportConfirmation()
}
private fun initUI() {
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), RequestCode.PERMISSION_WRITE_EXTERNAL_STORAGE.value) {
val path = Realm.getDefaultInstance().path
contentResolver.openInputStream(fileURI)?.let { inputStream ->
val destination = File(path)
inputStream.copyStreamToFile(destination)
toast("Please restart app")
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RequestCode.IMPORT.value -> {
if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) {
showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = {
finish()
})
}
}
}
}
// Import
private fun requestImportConfirmation() {
showAlertDialog(context = this, title = R.string.import_confirmation, showCancelButton = true, positiveAction = {
initUI()
}, negativeAction = {
finish()
})
}
}

@ -9,31 +9,14 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.databinding.ActivityHomeBinding import net.pokeranalytics.android.databinding.ActivityHomeBinding
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Currency import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.BackupTask
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.findAll
import net.pokeranalytics.android.util.extensions.isSameMonth
import java.util.*
enum class Tab(var identifier: Int) {
HISTORY(0),
STATS(1),
CALENDAR(2),
REPORTS(3),
SETTINGS(4)
}
class HomeActivity : BaseActivity(), NewPerformanceListener { class HomeActivity : BaseActivity() {
companion object { companion object {
fun newInstance(context: Context, id: Int) { fun newInstance(context: Context, id: Int) {
@ -47,29 +30,23 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
private var homePagerAdapter: HomePagerAdapter? = null private var homePagerAdapter: HomePagerAdapter? = null
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (binding.viewPager.currentItem == Tab.REPORTS.identifier) {
this.paApplication.reportWhistleBlower?.clearNotifications()
}
when (item.itemId) { when (item.itemId) {
R.id.navigation_history -> { R.id.navigation_history -> {
displayFragment(Tab.HISTORY) displayFragment(0)
} }
R.id.navigation_stats -> { R.id.navigation_stats -> {
displayFragment(Tab.STATS) displayFragment(1)
} }
R.id.navigation_calendar -> { R.id.navigation_calendar -> {
displayFragment(Tab.CALENDAR) displayFragment(2)
} }
R.id.navigation_reports -> { R.id.navigation_reports -> {
displayFragment(Tab.REPORTS) displayFragment(3)
} }
R.id.navigation_settings -> { R.id.navigation_settings -> {
displayFragment(Tab.SETTINGS) displayFragment(4)
} }
} }
binding.navigation.getOrCreateBadge(item.itemId).isVisible = false
return@OnNavigationItemSelectedListener true return@OnNavigationItemSelectedListener true
} }
@ -77,8 +54,6 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
super.onResume() super.onResume()
AppGuard.requestPurchasesUpdate() AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed() this.homePagerAdapter?.activityResumed()
lookForCalendarBadge()
checkForFailedBackups()
} }
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
@ -102,13 +77,6 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
initUI() initUI()
checkFirstLaunch() checkFirstLaunch()
this.paApplication.reportWhistleBlower?.addListener(this)
}
override fun onDestroy() {
super.onDestroy()
this.paApplication.reportWhistleBlower?.removeListener(this)
} }
private fun observeRealmObjects() { private fun observeRealmObjects() {
@ -117,9 +85,9 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
// observe currency changes // observe currency changes
this.currencies = realm.where(Currency::class.java).findAll() this.currencies = realm.where(Currency::class.java).findAll()
this.currencies.addChangeListener { currencies, _ -> this.currencies.addChangeListener { t, _ ->
realm.executeTransaction { realm.executeTransaction {
currencies.forEach { t.forEach {
it.refreshRelatedRatedValues() it.refreshRelatedRatedValues()
} }
} }
@ -140,7 +108,6 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
viewPager.offscreenPageLimit = 5 viewPager.offscreenPageLimit = 5
viewPager.enablePaging = false viewPager.enablePaging = false
viewPager.adapter = homePagerAdapter viewPager.adapter = homePagerAdapter
} }
/** /**
@ -160,70 +127,8 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
/** /**
* Display a new fragment * Display a new fragment
*/ */
private fun displayFragment(tab: Tab) { private fun displayFragment(index: Int) {
binding.viewPager.setCurrentItem(tab.identifier, false) binding.viewPager.setCurrentItem(index, false)
this.homePagerAdapter?.tabSelected(tab)
}
override fun newBestPerformanceHandler() {
if (Preferences.showInAppBadges(this)) {
binding.navigation.getOrCreateBadge(R.id.navigation_reports).isVisible = true
binding.navigation.getOrCreateBadge(R.id.navigation_reports).number = 1
}
}
private fun lookForCalendarBadge() {
if (!Preferences.showInAppBadges(this)) {
return
}
val date = Preferences.lastCalendarBadgeDate(this)
val cal = Calendar.getInstance()
val lastCheck = Calendar.getInstance().apply { timeInMillis = date }
if (!cal.isSameMonth(lastCheck)) {
lookForSessionsLastMonth()
}
}
private fun lookForSessionsLastMonth() {
val cal = Calendar.getInstance()
cal.add(Calendar.MONTH, -1)
val month = QueryCondition.AnyMonthOfYear(cal.get(Calendar.MONTH))
val year = QueryCondition.AnyYear(cal.get(Calendar.YEAR))
val query = Query(month, year)
val sessions = getRealm().findAll<Session>(query)
if (sessions.isNotEmpty()) {
binding.navigation.getOrCreateBadge(R.id.navigation_calendar).isVisible = true
binding.navigation.getOrCreateBadge(R.id.navigation_calendar).number = 1
Preferences.setLastCalendarBadgeDate(this, Date().time)
}
}
private fun checkForFailedBackups() {
if (!Preferences.sessionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.SESSION, email, this)
task.start()
}
}
if (!Preferences.transactionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.TRANSACTION, email, this)
task.start()
}
}
} }
} }

@ -18,6 +18,7 @@ import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.fragment.ImportFragment import net.pokeranalytics.android.ui.fragment.ImportFragment
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.extensions.count import net.pokeranalytics.android.util.extensions.count
import timber.log.Timber
class ImportActivity : BaseActivity() { class ImportActivity : BaseActivity() {
@ -63,13 +64,11 @@ class ImportActivity : BaseActivity() {
val fragmentTransaction = supportFragmentManager.beginTransaction() val fragmentTransaction = supportFragmentManager.beginTransaction()
val fragment = ImportFragment() val fragment = ImportFragment()
fragment.setData(fileURI) val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis")
// val fis = contentResolver.openInputStream(fileURI) fis?.let {
// Timber.d("Load fragment data with: $fis") fragment.setData(it)
// fis?.let { }
// fragment.setData(it)
// }
fragmentTransaction.add(R.id.container, fragment) fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit() fragmentTransaction.commit()
@ -84,7 +83,7 @@ class ImportActivity : BaseActivity() {
when (requestCode) { when (requestCode) {
RequestCode.IMPORT.value -> { RequestCode.IMPORT.value -> {
if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) { if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) {
showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = { showAlertDialog(context = this, message = R.string.unknown_import_format_popup_message, positiveAction = {
finish() finish()
}) })
} }

@ -4,7 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calcul.ReportDisplay
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.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity

@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode

@ -3,7 +3,6 @@ package net.pokeranalytics.android.ui.activity.components
import android.Manifest.permission.ACCESS_FINE_LOCATION import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
import android.view.MenuItem import android.view.MenuItem
@ -14,16 +13,13 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.model.realm.Location import net.pokeranalytics.android.model.realm.Location
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.helpers.AppReviewManager import net.pokeranalytics.android.ui.helpers.AppReviewManager
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.LocationManager import net.pokeranalytics.android.util.LocationManager
import net.pokeranalytics.android.util.PermissionRequest import net.pokeranalytics.android.util.PermissionRequest
import net.pokeranalytics.android.util.Preferences
import java.util.*
class RootBottomSheetViewModel: ViewModel() { class RootBottomSheetViewModel: ViewModel() {
var rowRepresentable: RowRepresentable? = null var rowRepresentable: RowRepresentable? = null
@ -41,9 +37,6 @@ abstract class BaseActivity : AppCompatActivity() {
private var permissionRequest: PermissionRequest? = null private var permissionRequest: PermissionRequest? = null
val paApplication: PokerAnalyticsApplication
get() { return this.application as PokerAnalyticsApplication }
val bottomSheetViewModel: RootBottomSheetViewModel by lazy { val bottomSheetViewModel: RootBottomSheetViewModel by lazy {
ViewModelProvider(this).get(RootBottomSheetViewModel::class.java) ViewModelProvider(this).get(RootBottomSheetViewModel::class.java)
} }
@ -58,14 +51,12 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState") CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState")
setLanguage()
} }
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState) super.onCreate(savedInstanceState, persistentState)
CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState") CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState")
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash
setLanguage()
} }
override fun onResume() { override fun onResume() {
@ -134,27 +125,6 @@ abstract class BaseActivity : AppCompatActivity() {
fragmentTransaction.commit() fragmentTransaction.commit()
} }
private fun setLanguage() {
Preferences.getLanguageCode(this)?.let { languageCode ->
val config = resources.configuration
// val lang = "de" // your language code
val locale = Locale(languageCode)
Locale.setDefault(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
config.setLocale(locale)
} else {
config.locale = locale
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
createConfigurationContext(config)
resources.updateConfiguration(config, resources.displayMetrics)
}
}
/** /**
* Return the realm instance * Return the realm instance
*/ */

@ -1,187 +0,0 @@
package net.pokeranalytics.android.ui.activity.components
import android.Manifest
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.databinding.ActivityCameraBinding
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraActivity : BaseActivity() {
companion object {
const val IMAGE_URI = "image_uri"
fun newInstanceForResult(fragment: Fragment, code: RequestCode) {
val intent = Intent(fragment.requireContext(), CameraActivity::class.java)
fragment.startActivityForResult(intent, code.value)
}
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
private lateinit var viewBinding: ActivityCameraBinding
private lateinit var cameraExecutor: ExecutorService
private var imageCapture: ImageCapture? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.viewBinding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
if (allPermissionsGranted()) {
cameraExecutor = Executors.newSingleThreadExecutor()
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
override fun onDestroy() {
super.onDestroy()
this.cameraExecutor.shutdown()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(this.viewBinding.viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder().build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Timber.e(exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time stamped name and MediaStore entry.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(e: ImageCaptureException) {
Timber.e(e)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
// val msg = "Photo capture succeeded: ${output.savedUri}"
// Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
// Timber.d(msg)
val intent = Intent()
intent.putExtra(IMAGE_URI, output.savedUri.toString())
setResult(Activity.RESULT_OK, intent)
finish()
}
}
)
}
}

@ -16,8 +16,7 @@ enum class RequestCode(var value: Int) {
IMPORT(900), IMPORT(900),
SUBSCRIPTION(901), SUBSCRIPTION(901),
CURRENCY(902), CURRENCY(902),
PERMISSION_WRITE_EXTERNAL_STORAGE(1000), PERMISSION_WRITE_EXTERNAL_STORAGE(1000)
CAMERA(1001)
} }
enum class ResultCode(var value: Int) { enum class ResultCode(var value: Int) {

@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calcul.ReportDisplay
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.ui.viewmodel.ReportViewModel import net.pokeranalytics.android.ui.viewmodel.ReportViewModel

@ -2,17 +2,14 @@ package net.pokeranalytics.android.ui.adapter
import android.util.SparseArray import android.util.SparseArray
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.util.forEach
import androidx.core.util.size
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.Tab import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment
import net.pokeranalytics.android.ui.fragment.ReportsFragment import net.pokeranalytics.android.ui.fragment.ReportsFragment
import net.pokeranalytics.android.ui.fragment.SettingsFragment import net.pokeranalytics.android.ui.fragment.SettingsFragment
import net.pokeranalytics.android.ui.fragment.StatisticsFragment import net.pokeranalytics.android.ui.fragment.StatisticsFragment
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment
import net.pokeranalytics.android.ui.modules.feed.FeedFragment import net.pokeranalytics.android.ui.modules.feed.FeedFragment
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -26,17 +23,17 @@ class HomePagerAdapter(fragmentManager: FragmentManager) :
override fun getItem(position: Int): BaseFragment { override fun getItem(position: Int): BaseFragment {
return when (position) { return when (position) {
Tab.HISTORY.identifier -> FeedFragment.newInstance() 0 -> FeedFragment.newInstance()
Tab.STATS.identifier -> StatisticsFragment.newInstance() 1 -> StatisticsFragment.newInstance()
Tab.CALENDAR.identifier -> CalendarFragment.newInstance() 2 -> CalendarFragment.newInstance()
Tab.REPORTS.identifier -> ReportsFragment.newInstance() 3 -> ReportsFragment.newInstance()
Tab.SETTINGS.identifier -> SettingsFragment.newInstance() 4 -> SettingsFragment.newInstance()
else -> throw PAIllegalStateException("Should not happen, position = $position") else -> throw PAIllegalStateException("Should not happen, position = $position")
} }
} }
override fun getCount(): Int { override fun getCount(): Int {
return Tab.values().size return 5
} }
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
@ -52,26 +49,19 @@ class HomePagerAdapter(fragmentManager: FragmentManager) :
override fun getItemPosition(obj: Any): Int { override fun getItemPosition(obj: Any): Int {
return when (obj) { return when (obj) {
is FeedFragment -> Tab.HISTORY.identifier is FeedFragment -> 0
is StatisticsFragment -> Tab.STATS.identifier is StatisticsFragment -> 1
is CalendarFragment -> Tab.CALENDAR.identifier is CalendarFragment -> 2
is ReportsFragment -> Tab.REPORTS.identifier is ReportsFragment -> 3
is SettingsFragment -> Tab.SETTINGS.identifier is SettingsFragment -> 4
else -> throw PAIllegalStateException("Should not happen for object $obj") else -> throw PAIllegalStateException("Should not happen for object $obj")
} }
} }
fun activityResumed() { fun activityResumed() {
val ref = this.weakReferences.get(0)
this.weakReferences.forEach { _, value -> ref?.get()?.let {
value.get()?.activityResumed() (it as FeedFragment).activityResumed()
}
}
fun tabSelected(tab: Tab) {
if (this.weakReferences.size >= 5) {
this.weakReferences.get(tab.identifier).get()?.selectedTab()
} }
} }

@ -68,4 +68,5 @@ class RowRepresentableAdapter(
this.dataSource = dataSource this.dataSource = dataSource
} }
} }

@ -3,17 +3,20 @@ package net.pokeranalytics.android.ui.extensions
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.text.SpannableStringBuilder
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -82,11 +85,7 @@ fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null
filePath?.let { filePath?.let {
val databaseFile = File(it) val databaseFile = File(it)
val contentUri = FileProvider.getUriForFile( val contentUri = FileProvider.getUriForFile(this, "net.pokeranalytics.android.fileprovider", databaseFile)
this,
"net.pokeranalytics.android.fileprovider",
databaseFile
)
if (contentUri != null) { if (contentUri != null) {
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri)) emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri))
@ -109,12 +108,7 @@ fun Context.openUrl(url: String) {
} }
// Open custom tab // Open custom tab
fun Context.areYouSure( fun Context.areYouSure(title: Int? = null, message: Int? = null, positiveTitle: Int? = null, proceed: () -> Unit) {
title: Int? = null,
message: Int? = null,
positiveTitle: Int? = null,
proceed: () -> Unit
) {
val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this) val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this)
@ -130,7 +124,22 @@ fun Context.areYouSure(
builder.setNegativeButton(R.string.cancel) { _, _ -> builder.setNegativeButton(R.string.cancel) { _, _ ->
// nothing // nothing
} }
// builder.setItems(
// arrayOf<CharSequence>(
// getString(R.string.yes),
// getString(R.string.cancel)
// )
// ) { _, index ->
// // The 'which' argument contains the index position
// // of the selected item
// when (index) {
// 0 -> proceed()
// 1 -> {} // nothing
// }
// }
builder.create().show() builder.create().show()
} }
// Display Alert Dialog // Display Alert Dialog
@ -138,77 +147,26 @@ fun Activity.showAlertDialog(title: Int? = null, message: Int? = null) {
showAlertDialog(this, title, message) showAlertDialog(this, title, message)
} }
fun Fragment.showAlertDialog( fun Fragment.showAlertDialog(title: Int? = null, message: Int? = null) {
title: Int? = null,
messageResId: Int? = null,
message: String? = null
) {
context?.let { context?.let {
showAlertDialog(it, title, messageResId, message) showAlertDialog(it, title, message)
}
} }
fun showEditTextAlertDialog(
context: Context, inputType: Int, title: Int? = null, messageResId: Int? = null, message: String? = null,
editTextText: String? = null, positiveAction: ((String) -> Unit)? = null
) {
val builder = AlertDialog.Builder(context)
title?.let {
builder.setTitle(title)
}
messageResId?.let {
builder.setMessage(messageResId)
}
message?.let {
builder.setMessage(it)
}
val layout = LinearLayout(context)
layout.orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
)
params.setMargins(20, 0, 30, 0)
val editText = EditText(context)
editText.inputType = inputType
editTextText?.let {
editText.text = SpannableStringBuilder(it)
}
editText.setTextColor(ContextCompat.getColor(context, R.color.white))
layout.addView(editText, params)
builder.setView(layout)
// builder.setView(editText)
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
positiveAction?.invoke(editText.text.toString())
}
builder.show()
} }
/** /**
* Create and show an alert dialog * Create and show an alert dialog
*/ */
fun showAlertDialog( fun showAlertDialog(
context: Context, title: Int? = null, messageResId: Int? = null, message: String? = null, context: Context, title: Int? = null, message: Int? = null, cancelButtonTitle: Int? = null, showCancelButton: Boolean = false,
cancelButtonTitle: Int? = null, showCancelButton: Boolean = false,
positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null
) { ) {
val builder = AlertDialog.Builder(context) val builder = AlertDialog.Builder(context)
title?.let { title?.let {
builder.setTitle(title) builder.setTitle(title)
} }
messageResId?.let {
builder.setMessage(messageResId)
}
message?.let { message?.let {
builder.setMessage(it) builder.setMessage(message)
} }
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
positiveAction?.invoke() positiveAction?.invoke()
} }

@ -45,47 +45,28 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
) )
} }
private val availableCurrencies = private val availableCurrencies = this.systemCurrencies.filter {
Locale.getAvailableLocales() !mostUsedCurrencyCodes.contains(it.currencyCode)
.mapNotNull { }.filter {
try { UserDefaults.availableCurrencyLocales.filter { currencyLocale ->
Currency.getInstance(it)
} catch (e: Exception) {
null
}
}.toSet()
.filter { !mostUsedCurrencyCodes.contains(it.currencyCode) }
.filter {
UserDefaults.availableCurrencyLocales.any { currencyLocale ->
Currency.getInstance(currencyLocale).currencyCode == it.currencyCode Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
}.isNotEmpty()
}.sortedBy {
it.displayName
}.map {
CurrencyRow(it)
} }
} }
.sortedBy { it.displayName }
.map { CurrencyRow(it) }
// private val availableCurrencies = this.systemCurrencies.filter {
// !mostUsedCurrencyCodes.contains(it.currencyCode)
// }.filter {
// UserDefaults.availableCurrencyLocales.filter { currencyLocale ->
// Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
// }.isNotEmpty()
// }.sortedBy {
// it.displayName
// }.map {
// CurrencyRow(it)
// }
}
private class CurrencyRow(var currency: Currency) : RowRepresentable { private class CurrencyRow(var currency: Currency) : RowRepresentable {
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.currency.getDisplayName(Locale.getDefault()).capitalize() return currency.getDisplayName(Locale.getDefault()).capitalize()
} }
var currencyCode: String = this.currency.currencyCode var currencyCode: String = currency.currencyCode
var currencySymbol: String = this.currency.getSymbol(Locale.getDefault()) var currencySymbole: String = currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})" var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
} }

@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.charts.LineChart
@ -72,8 +70,7 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
val styleIndex = bundle.getInt(BundleKey.STYLE.value) val styleIndex = bundle.getInt(BundleKey.STYLE.value)
return Graph.Style.values()[styleIndex] return Graph.Style.values()[styleIndex]
} else { } else {
return Graph.Style.LINE throw PAIllegalStateException("Style not defined for $this")
// throw PAIllegalStateException("Style not defined for $this")
} }
} ?: throw PAIllegalStateException("Style and bundle not defined for $this") } ?: throw PAIllegalStateException("Style and bundle not defined for $this")
} }
@ -103,7 +100,6 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
initData() initData()
initUI() initUI()
loadGraph() loadGraph()
} }
private fun initData() { private fun initData() {

@ -1,13 +1,11 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
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.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -16,7 +14,6 @@ import net.pokeranalytics.android.databinding.FragmentImportBinding
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter import net.pokeranalytics.android.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate import net.pokeranalytics.android.util.csv.ImportDelegate
import net.pokeranalytics.android.util.csv.ImportException
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.text.NumberFormat import java.text.NumberFormat
@ -24,9 +21,11 @@ import java.util.*
class ImportFragment : RealmFragment(), ImportDelegate { class ImportFragment : RealmFragment(), ImportDelegate {
// val coroutineContext: CoroutineContext
// get() = Dispatchers.Main
private lateinit var filePath: String private lateinit var filePath: String
private lateinit var inputStream: InputStream private lateinit var inputStream: InputStream
private lateinit var uri: Uri
private lateinit var importer: CSVImporter private lateinit var importer: CSVImporter
private var _binding: FragmentImportBinding? = null private var _binding: FragmentImportBinding? = null
@ -55,10 +54,6 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.inputStream = inputStream this.inputStream = inputStream
} }
fun setData(uri: Uri) {
this.uri = uri
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -91,29 +86,26 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private fun startImport() { private fun startImport() {
this.parentActivity?.paApplication?.reportWhistleBlower?.pause() // var shouldDismissActivity = false
this.importer = CSVImporter(uri, requireContext()) this.importer = CSVImporter(inputStream)
this.importer.delegate = this this.importer.delegate = this
CoroutineScope(coroutineContext).launch {
val coroutine = GlobalScope.async { GlobalScope.launch(coroutineContext) {
val test = GlobalScope.async {
val s = Date() val s = Date()
Timber.d(">>> Start Import...") Timber.d(">>> Start Import...")
try {
importer.start() importer.start()
} catch (e: ImportException) {
exceptions.add(e)
}
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
Timber.d(">>> Import ended in $duration seconds") Timber.d(">>> Import ended in $duration seconds")
} }
coroutine.await() test.await()
val exceptionMessage = exceptions.firstOrNull()?.message val exceptionMessage = exceptions.firstOrNull()?.message
if (exceptionMessage != null && view != null) { if (exceptionMessage != null && view != null) {
@ -127,6 +119,15 @@ class ImportFragment : RealmFragment(), ImportDelegate {
snackBar.show() snackBar.show()
} }
// if (shouldDismissActivity) {
//
// activity?.let {
// it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value)
// it.finish()
// }
//
// } else {
// }
importDidFinish() importDidFinish()
} }
@ -138,11 +139,12 @@ class ImportFragment : RealmFragment(), ImportDelegate {
} }
private fun importDidFinish() { private fun importDidFinish() {
binding.save.isEnabled = true binding.save.isEnabled = true
} }
private fun end() { private fun end() {
this.parentActivity?.paApplication?.reportWhistleBlower?.resume()
activity?.finish() activity?.finish()
} }

@ -7,7 +7,7 @@ import android.view.*
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.databinding.FragmentReportCreationBinding import net.pokeranalytics.android.databinding.FragmentReportCreationBinding

@ -1,37 +1,29 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
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.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.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding import net.pokeranalytics.android.databinding.FragmentReportsBinding
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.model.realm.Result import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -39,44 +31,16 @@ 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
import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.StaticReport import net.pokeranalytics.android.ui.view.rows.ReportRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences
import timber.log.Timber import timber.log.Timber
import java.util.Date import java.util.*
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
fun getDisplayName(context: Context): String {
return when (report) {
is StaticReport.CustomFieldList -> {
report.customField.name
}
else -> {
this.report.resId?.let { context.getString(it) } ?: NULL_TEXT
}
}
}
}
data class PerformanceRow(val performance: Performance, val report: StaticReport): RowRepresentable {
override val resId: Int? = this.performance.resId
override val viewType: Int = RowViewType.TITLE_BADGE_VALUE.identifier
}
class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate, NewPerformanceListener { class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
private lateinit var reportSetups: RealmResults<ReportSetup> private lateinit var reportSetups: RealmResults<ReportSetup>
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> {
@ -136,22 +100,15 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
} else if (requestCode == RequestCode.DEFAULT.value && resultCode == Activity.RESULT_OK) { } else if (requestCode == RequestCode.DEFAULT.value && resultCode == Activity.RESULT_OK) {
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(coroutineContext).launch {
delay(300) delay(300)
deleteItem(dataListAdapter, reportSetups, id) deleteItem(dataListAdapter, reportSetups, id)
} }
} }
} }
} }
override fun selectedTab() {
// this.updateRows()
this.dataListAdapter.notifyDataSetChanged()
}
// Business // Business
/** /**
@ -162,11 +119,6 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
this.reportSetups.addChangeListener { _, _ -> this.reportSetups.addChangeListener { _, _ ->
this.updateRows() this.updateRows()
} }
this.performances = getRealm().where(Performance::class.java).findAll()
this.performances.addChangeListener { _, _ ->
this.updateRows()
}
} }
/** /**
@ -192,112 +144,31 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
ReportCreationActivity.newInstanceForResult(this, requireContext()) ReportCreationActivity.newInstanceForResult(this, requireContext())
} }
val sessionCount = getRealm().where(Result::class.java).count()
binding.computeButton.isVisible = adapterRows.isEmpty() && sessionCount > 5
binding.computeButton.setOnClickListener {
try {
forceReportWhistleBlowerStart()
} catch (e: Exception) {
e.message?.let {
this.showSnackBar(it)
}
}
}
this.paApplication?.reportWhistleBlower?.addListener(this)
}
override fun onDestroy() {
super.onDestroy()
this.paApplication?.reportWhistleBlower?.removeListener(this)
} }
// Rows // Rows
private fun updateRows() { private fun updateRows() {
this.adapterRows.clear() this.adapterRows.clear()
if (this.reportSetups.isNotEmpty()) { if (this.reportSetups.size > 0) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom)) adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups) adapterRows.addAll(this.reportSetups)
} }
this.addStaticReportRows() adapterRows.addAll(ReportRow.getRows())
this.binding.emptyScreenText.visibility = if (adapterRows.isEmpty()) { View.VISIBLE } else { View.GONE }
this.dataListAdapter.notifyDataSetChanged() this.dataListAdapter.notifyDataSetChanged()
} }
private fun addStaticReportRows() {
context?.let { context ->
val sections = buildReportSections()
for (section in sections) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, title = section.getDisplayName(context)))
for (performance in section.performances) {
adapterRows.add(performance)
}
}
}
}
private fun buildReportSections(): List<ReportSection> {
val sections = mutableListOf<ReportSection>()
val performances = getRealm().where(Performance::class.java).findAll()
for (performance in performances) {
val report = performance.toStaticReport(getRealm())
val reportRow = PerformanceRow(performance, report)
sections.firstOrNull { it.report == report }?.let { section ->
section.performances.add(reportRow)
} ?: run {
val section = ReportSection(report, mutableListOf(reportRow))
sections.add(section)
}
}
return sections
}
override fun adapterRows(): List<RowRepresentable> { override fun adapterRows(): List<RowRepresentable> {
return this.adapterRows return this.adapterRows
} }
override fun charSequenceForRow(row: RowRepresentable, context: Context): CharSequence {
return when (row) {
is PerformanceRow -> {
row.performance.displayValue(getRealm())
}
else -> NULL_TEXT
}
}
/**
* Returns whether the row should display a badge
*/
override fun boolForRow(row: RowRepresentable): Boolean {
val reportRow = row as PerformanceRow
return Preferences.showInAppBadges(requireContext())
&& (this.paApplication?.reportWhistleBlower?.has(reportRow.performance.id) ?: false)
}
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
super.onRowSelected(position, row, tag) super.onRowSelected(position, row, tag)
when (row) { when (row) {
is PerformanceRow -> { is ReportRow -> {
val reportName = row.localizedTitle(requireContext()) val reportName = row.localizedTitle(requireContext())
val report = row.report launchComputation(row.criteria, reportName)
if (report.hasGraph) {
launchComputation(report.criteria, reportName, row.performance.stat)
}
} }
is ReportSetup -> { is ReportSetup -> {
val display = ReportDisplay.values()[row.display] val display = ReportDisplay.values()[row.display]
@ -309,16 +180,17 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
/** /**
* Launch computation * Launch computation
*/ */
private fun launchComputation(criteriaList: List<Criteria>, reportName: String, stat: Stat) { private fun launchComputation(criteriaList: List<Criteria>, reportName: String) {
if (criteriaList.combined().size < 2) { if (criteriaList.combined().size < 2) {
Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show()
return return
} }
val requiredStats: List<Stat> = listOf(Stat.NET_RESULT)
val options = Calculator.Options( val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD, progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = listOf(stat), stats = requiredStats,
criterias = criteriaList criterias = criteriaList
) )
@ -333,7 +205,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
showLoader() showLoader()
CoroutineScope(coroutineContext).launch { GlobalScope.launch {
val startDate = Date() val startDate = Date()
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -353,41 +225,4 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
} }
} }
override fun newBestPerformanceHandler() {
Timber.d("newBestPerformanceHandler called")
activity?.runOnUiThread {
this.dataListAdapter.notifyDataSetChanged()
}
}
private fun forceReportWhistleBlowerStart() {
val rwb = ReportWhistleBlower(requireContext())
val reportTask = ReportTask(rwb, requireContext())
reportTask.handler = {
Timber.d("test")
val paused = paApplication?.reportWhistleBlower?.paused
reportTask.messages.add(">>> main RWB paused = $paused")
val message = reportTask.messages.joinToString("\n")
CoroutineScope(coroutineContext).launch(Dispatchers.Main) {
Timber.d("test2")
copyToClipboard(requireContext(), message)
}
}
reportTask.start()
}
fun copyToClipboard(context: Context, text: String) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("label", text)
clipboard.setPrimaryClip(clip)
}
} }

@ -6,51 +6,37 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.api.CurrencyConverterApi
import net.pokeranalytics.android.databinding.FragmentSettingsBinding import net.pokeranalytics.android.databinding.FragmentSettingsBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.Currency import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.activity.BillingActivity import net.pokeranalytics.android.ui.activity.*
import net.pokeranalytics.android.ui.activity.CurrenciesActivity
import net.pokeranalytics.android.ui.activity.GDPRActivity
import net.pokeranalytics.android.ui.activity.Top10Activity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
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
import net.pokeranalytics.android.ui.extensions.openContactMail import net.pokeranalytics.android.ui.extensions.openContactMail
import net.pokeranalytics.android.ui.extensions.openUrl import net.pokeranalytics.android.ui.extensions.openUrl
import net.pokeranalytics.android.ui.extensions.showEditTextAlertDialog
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.FileUtils import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.Language
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.StopNotificationManager
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
@ -59,7 +45,7 @@ import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.Date import java.util.*
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener { class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -76,12 +62,11 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
return fragment return fragment
} }
// fun rowRepresentation(context: Context): List<RowRepresentable> { val rowRepresentation: List<RowRepresentable> by lazy {
// val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
// val hasBackupEmail = Preferences.getBackupEmail(context)?.isNotBlank() ?: false rows.addAll(SettingsRow.getRows())
// rows.addAll(SettingsRow.getRows(hasBackupEmail)) rows
// return rows }
// }
} }
@ -120,32 +105,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
initUI() initUI()
} }
/**
* Init UI
*/
private fun initUI() {
val viewManager = LinearLayoutManager(requireContext())
settingsAdapterRow = RowRepresentableAdapter(this, this)
binding.recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = settingsAdapterRow
}
}
/**
* Init data
*/
private fun initData() {
}
override fun activityResumed() {
this.settingsAdapterRow.notifyDataSetChanged()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
@ -153,62 +112,30 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
data?.let { data?.let {
val currencyCode = data.getStringExtra(CurrenciesFragment.INTENT_CURRENCY_CODE) ?: throw PAIllegalStateException("Missing currency code") val currencyCode = data.getStringExtra(CurrenciesFragment.INTENT_CURRENCY_CODE) ?: throw PAIllegalStateException("Missing currency code")
updateMainCurrency(currencyCode)
}
}
}
RequestCode.SUBSCRIPTION.value -> {
settingsAdapterRow.refreshRow(SettingsRow.SUBSCRIPTION)
}
}
}
private fun updateMainCurrency(currencyCode: String) {
val mainCurrencyCode = UserDefaults.currency.currencyCode
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ ->
hideLoader()
val message = requireContext().getString(R.string.currency_rate_confirmation, mainCurrencyCode, currencyCode)
context?.let { context ->
showEditTextAlertDialog(context, InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL,
message = message, editTextText = apiRate?.toString()) { value ->
value.toDoubleOrNull()?.let { rate ->
updateMainCurrency(currencyCode, rate)
}
}
}
}
}
private fun updateMainCurrency(currencyCode: String, rate: Double) {
Preferences.setCurrencyCode(currencyCode, requireContext()) Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransaction {
realm.where(Currency::class.java).findAll().forEach { currency -> realm.where(Currency::class.java).isNull("code").or().equalTo("code", UserDefaults.currency.currencyCode).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate currency.rate = Currency.DEFAULT_RATE
} }
realm.where(Session::class.java).findAll().forEach { session -> realm.where(Session::class.java).isNull("bankroll.currency.code").findAll().forEach { session ->
session.bankrollHasBeenUpdated() session.bankrollHasBeenUpdated()
} }
} }
realm.close() realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY) settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
} }
}
}
RequestCode.SUBSCRIPTION.value -> {
settingsAdapterRow.refreshRow(SettingsRow.SUBSCRIPTION)
}
}
}
override fun adapterRows(): List<RowRepresentable> { override fun adapterRows(): List<RowRepresentable> {
return SettingsRow.getRows() return rowRepresentation
} }
override fun charSequenceForRow( override fun charSequenceForRow(
@ -220,7 +147,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext()) SettingsRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext())
SettingsRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else "" SettingsRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else ""
SettingsRow.CURRENCY -> UserDefaults.currency.symbol SettingsRow.CURRENCY -> UserDefaults.currency.symbol
SettingsRow.BACKUP_EMAIL -> Preferences.getBackupEmail(requireContext()) ?: ""
else -> "" else -> ""
} }
} }
@ -229,8 +155,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
return when (row) { return when (row) {
SettingsRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext()) SettingsRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext())
SettingsRow.SHOULD_SHOW_BLOG_TIPS -> Preferences.shouldShowBlogTips(requireContext()) SettingsRow.SHOULD_SHOW_BLOG_TIPS -> Preferences.shouldShowBlogTips(requireContext())
SettingsRow.SHOW_INAPP_BADGES -> Preferences.showInAppBadges(requireContext())
SettingsRow.BACKUP_EMAIL -> !Preferences.hasBackupEmail(requireContext())
else -> false else -> false
} }
} }
@ -247,13 +171,11 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
this.openPlayStoreAccount() this.openPlayStoreAccount()
} }
} }
SettingsRow.LANGUAGE -> this.showLanguagePopup()
SettingsRow.RATE_APP -> showReviewManager() SettingsRow.RATE_APP -> showReviewManager()
SettingsRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact) SettingsRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact)
SettingsRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path) SettingsRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingsRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value) SettingsRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingsRow.DEALT_HANDS_PER_HOUR -> DealtHandsPerHourActivity.newInstance(requireContext()) SettingsRow.DEALT_HANDS_PER_HOUR -> DealtHandsPerHourActivity.newInstance(requireContext())
SettingsRow.BACKUP_EMAIL -> this.editBackupEmail()
SettingsRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport() SettingsRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport()
SettingsRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport() SettingsRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport()
SettingsRow.FOLLOW_US -> { SettingsRow.FOLLOW_US -> {
@ -277,40 +199,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
} }
} }
private fun showLanguagePopup() {
context?.let { context ->
val languages = Language.values()
val labels = Language.values().map { it.dualNames }
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
builder.setTitle(R.string.language_popup_message)
builder.setItems(labels.toTypedArray()) { _, which ->
// the user clicked on colors[which]
val locale = languages[which]
Preferences.setLanguageCode(locale.code, context)
this.view?.let { view ->
val snackBar = Snackbar.make(view, R.string.language_should_restart_app_popup_message, Snackbar.LENGTH_INDEFINITE)
snackBar.show()
}
}
builder.show()
}
}
private fun editBackupEmail() {
context?.let { context ->
showEditTextAlertDialog(context, InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, messageResId = R.string.backup_email_title, editTextText = Preferences.getBackupEmail(context)) { value ->
Preferences.setBackupEmail(value, context)
this.settingsAdapterRow.notifyDataSetChanged()
}
}
}
private fun showReviewManager() { private fun showReviewManager() {
val manager = ReviewManagerFactory.create(requireContext()) val manager = ReviewManagerFactory.create(requireContext())
@ -324,8 +212,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed // completed
} }
} else { } else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result. // There was some problem, continue regardless of the result.
} }
} }
@ -345,14 +231,36 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
val show = value as Boolean val show = value as Boolean
Preferences.showBlogTips(show, requireContext()) Preferences.showBlogTips(show, requireContext())
} }
SettingsRow.SHOW_INAPP_BADGES -> {
val show = value as Boolean
Preferences.setShowInAppBadges(requireContext(), show)
}
else -> {} else -> {}
} }
} }
/**
* Init UI
*/
private fun initUI() {
// setToolbarTitle(getString(R.string.more))
// setDisplayHomeAsUpEnabled(true)
val viewManager = LinearLayoutManager(requireContext())
settingsAdapterRow = RowRepresentableAdapter(this, this)
binding.recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = settingsAdapterRow
}
}
/**
* Init data
*/
private fun initData() {
}
/** /**
* Open GDPR Activity * Open GDPR Activity
*/ */
@ -384,7 +292,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
private fun sessionsCSVExport() { private fun sessionsCSVExport() {
val sessions = getRealm().where(Session::class.java).findAll().sort("startDate") val sessions = getRealm().where(Session::class.java).findAll().sort("startDate")
val csv = ProductCSVDescriptors.pokerAnalyticsAndroid6Sessions.toCSV(sessions) val csv = ProductCSVDescriptors.pokerAnalyticsAndroidSessions.toCSV(sessions)
this.shareCSV(csv, "Sessions") this.shareCSV(csv, "Sessions")
} }

@ -2,13 +2,11 @@ package net.pokeranalytics.android.ui.fragment
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.LayoutInflater
import androidx.appcompat.widget.Toolbar import android.view.View
import android.view.ViewGroup
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -21,14 +19,16 @@ import net.pokeranalytics.android.databinding.FragmentStatsBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
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.* import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.model.realm.UserConfig
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.fragment.components.RealmAsyncListener
import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment
import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode
import net.pokeranalytics.android.ui.modules.filter.FilterableType 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 timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -36,8 +36,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
private lateinit var tableReportFragment: ComposableTableReportFragment private lateinit var tableReportFragment: ComposableTableReportFragment
private var transactionFilterMenuItem: MenuItem? = null
companion object { companion object {
/** /**
@ -75,7 +73,6 @@ 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)
} }
private fun initUI() { private fun initUI() {
@ -86,41 +83,27 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
this.tableReportFragment = fragment this.tableReportFragment = fragment
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { // override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java)
super.onCreateOptionsMenu(menu, inflater)
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar -> // override fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {
toolbar.menu.removeItem(R.id.menu_item_transaction_filter) // Timber.d("Entities changes, launch stats computation, size = ${results.size}")
transactionFilterMenuItem = toolbar.menu?.add(0, R.id.menu_item_transaction_filter, 1, R.string.filter) // val cr = results as RealmResults<ComputableResult>
transactionFilterMenuItem?.setIcon(R.drawable.baseline_payment_24) // cr.forEach {
transactionFilterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM) // Timber.d("### buyin = ${it.ratedBuyin} ### net result = ${it.ratedNet}")
setTransactionFilterItemColor() // }
} // this.launchStatComputation()
} // }
private fun setTransactionFilterItemColor() { // override fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
context?.let { // val rows: ArrayList<RowRepresentable> = ArrayList()
val userConfig = UserConfig.getConfiguration(getRealm()) // report.results.forEach { result ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white // rows.add(CustomizableRowRepresentable(title = result.group.name))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // result.group.stats?.forEach { stat ->
this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color)) // rows.add(StatRow(stat, result.computedStat(stat), result.group.name))
} // }
} // }
} // return rows
// }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_item_transaction_filter -> {
showTransactionFilter()
}
}
return super.onOptionsItemSelected(item)
}
private fun showTransactionFilter() {
context?.let {
TransactionFilterActivity.newInstance(it)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
@ -147,10 +130,18 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
// Business // Business
override fun asyncListenedEntityChange(realm: Realm) { override fun asyncListenedEntityChange(realm: Realm) {
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()
} }
// val report = createSessionGroupsAndStartCompute(realm)
// tableReportFragment.report = report
//
// GlobalScope.launch(Dispatchers.Main) {
// tableReportFragment.showResults()
// }
} }
/** /**
@ -158,11 +149,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/ */
private fun launchStatComputation() { private fun launchStatComputation() {
CoroutineScope(coroutineContext).launch { GlobalScope.launch(coroutineContext) {
val async = GlobalScope.async { val async = GlobalScope.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()
@ -174,7 +165,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
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()
@ -190,11 +181,8 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/ */
private fun createSessionGroupsAndStartCompute(realm: Realm): Report { private fun createSessionGroupsAndStartCompute(realm: Realm): Report {
var filter: Filter? = null val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let {
context?.let { context -> if (it.filterableType == currentFilterable) { it } else { null }
this.currentFilter(context, realm)?.let { current ->
if (current.filterableType == currentFilterable) { filter = current }
}
} }
val allStats: List<Stat> = listOf( val allStats: List<Stat> = listOf(
@ -241,7 +229,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats) val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats)
// Timber.d(">>>>> Start computations...") Timber.d(">>>>> Start computations...")
val options = Calculator.Options() val options = Calculator.Options()
val computedStats = mutableListOf<Stat>() val computedStats = mutableListOf<Stat>()
@ -250,8 +238,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
computedStats.addAll(tStats) computedStats.addAll(tStats)
options.stats = computedStats options.stats = computedStats
options.includedTransactions = UserConfig.getConfiguration(realm).transactionTypes(realm)
return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options) return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options)
} }

@ -30,13 +30,12 @@ import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.time.Period import java.time.Period
import java.time.format.DateTimeParseException import java.time.format.DateTimeParseException
class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, PurchaseListener, ViewPager.OnPageChangeListener { class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, PurchaseListener, ViewPager.OnPageChangeListener {
companion object { companion object {
val parallax: Float = 64f.px val parallax: Float = 64f.px
@ -51,9 +50,7 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
} }
private var pagerAdapter: ScreenSlidePagerAdapter? = null private var pagerAdapter: ScreenSlidePagerAdapter? = null
private var selectedProduct: ProductDetails? = null private var selectedProduct: SkuDetails? = null
private var selectedOfferDetails: ProductDetails.SubscriptionOfferDetails? = null
private var showSessionMessage = false private var showSessionMessage = false
private var _binding: FragmentSubscriptionBinding? = null private var _binding: FragmentSubscriptionBinding? = null
@ -76,7 +73,7 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
AppGuard.registerListener(this) AppGuard.registerListener(this)
if (!requireContext().isNetworkAvailable()) { if (!isNetworkAvailable()) {
Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show()
return return
} }
@ -159,22 +156,16 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
purchase.isEnabled = true purchase.isEnabled = true
purchase.setOnClickListener { purchase.setOnClickListener {
val network = requireContext().isNetworkAvailable() val network = isNetworkAvailable()
Timber.d("isNetworkAvailable = $network ") Timber.d("isNetworkAvailable = $network ")
if (!network) { if (!isNetworkAvailable()) {
Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show()
return@setOnClickListener return@setOnClickListener
} }
this.selectedProduct?.let { productDetails -> this.selectedProduct?.let {
AppGuard.initiatePurchase(this.requireActivity(), it)
this.selectedOfferDetails?.let { offerDetails ->
AppGuard.initiatePurchase(this.requireActivity(), productDetails, offerDetails.offerToken)
}?: run {
Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show()
}
} ?: run { } ?: run {
Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show()
} }
@ -234,71 +225,32 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
} }
// SkuDetailsResponseListener // SkuDetailsResponseListener
// override fun onSkuDetailsResponse(result: BillingResult, skuDetailsList: MutableList<SkuDetails>?) { override fun onSkuDetailsResponse(result: BillingResult, skuDetailsList: MutableList<SkuDetails>?) {
// if (result.responseCode == BillingClient.BillingResponseCode.OK) {
// this.hideLoader()
// selectedProduct = skuDetailsList?.firstOrNull { it.sku == IAPProducts.PRO.identifier }
// updateUI()
// }
// }
// ProductDetailsResponseListener
override fun onProductDetailsResponse(result: BillingResult, productList: MutableList<ProductDetails>) {
if (result.responseCode == BillingClient.BillingResponseCode.OK) { if (result.responseCode == BillingClient.BillingResponseCode.OK) {
this.hideLoader() this.hideLoader()
selectedProduct = productList.firstOrNull { it.productId == IAPProducts.PRO.identifier } selectedProduct = skuDetailsList?.firstOrNull { it.sku == IAPProducts.PRO.identifier }
this.selectedOfferDetails = selectedProduct?.subscriptionOfferDetails?.firstOrNull()
Timber.d("OFFERS = ${this.selectedProduct?.subscriptionOfferDetails?.size ?: 0}")
updateUI() updateUI()
} }
} }
private fun updateUI() { private fun updateUI() {
this.selectedProduct?.let { productDetails -> this.selectedProduct?.let {
var price: String? = null
var freeTrialPeriod: String? = null
productDetails.subscriptionOfferDetails?.firstOrNull()?.let { details ->
details.pricingPhases.pricingPhaseList.forEach { pricingPhase ->
when (pricingPhase.priceAmountMicros) {
0L -> {
freeTrialPeriod = pricingPhase.billingPeriod
}
else -> {
price = pricingPhase.formattedPrice
}
}
}
}
price?.let {
val perYearString = requireContext().getString(R.string.year_subscription) val perYearString = requireContext().getString(R.string.year_subscription)
val formattedPrice = "$it / $perYearString" val formattedPrice = it.price + " / " + perYearString
binding.price.text = formattedPrice binding.price.text = formattedPrice
}
freeTrialPeriod?.let {
var freeTrialDays = 30 // initial, should be more, no less var freeTrialDays = 30 // initial, should be more, no less
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try { try {
val p = Period.parse(it) val p = Period.parse(it.freeTrialPeriod)
freeTrialDays = p.days freeTrialDays = p.days
} catch (e: DateTimeParseException) { } catch (e: DateTimeParseException) {
CrashLogging.log("Error parsing period with value: $it") CrashLogging.log("Error parsing period with value: ${it.freeTrialPeriod}")
} }
} }
val formattedFreeTrial = val formattedFreeTrial =
"$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial) "$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial)
binding.freetrial.text = formattedFreeTrial binding.freetrial.text = formattedFreeTrial
}
} ?: run { } ?: run {
Toast.makeText(requireContext(), R.string.contact_support, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.contact_support, Toast.LENGTH_LONG).show()
} }
@ -309,7 +261,7 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
override fun purchaseDidSucceed(purchase: Purchase) { override fun purchaseDidSucceed(purchase: Purchase) {
// record purchase in preferences for troubleshooting / verification // record purchase in preferences for troubleshooting / verification
val purchaseInfos = listOf(purchase.products.joinToString(" - "), purchase.orderId, purchase.purchaseToken) val purchaseInfos = listOf(purchase.sku, purchase.orderId, purchase.purchaseToken)
Preferences.setString(Preferences.Keys.LATEST_PURCHASE, purchaseInfos.joinToString("/"), requireContext()) Preferences.setString(Preferences.Keys.LATEST_PURCHASE, purchaseInfos.joinToString("/"), requireContext())
this.activity?.finish() this.activity?.finish()
@ -334,7 +286,8 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
private fun updatePagerIndicators(position: Int) { private fun updatePagerIndicators(position: Int) {
binding.pageIndicator.children.forEachIndexed { index, view -> binding.pageIndicator.children.forEachIndexed { index, view ->
when (val drawable = view.background) { val drawable = view.background
when (drawable) {
is GradientDrawable -> { is GradientDrawable -> {
val color = if (position == index) R.color.white else R.color.quantum_grey val color = if (position == index) R.color.white else R.color.quantum_grey
drawable.setColor(requireContext().getColor(color)) drawable.setColor(requireContext().getColor(color))

@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.kotlin.where import io.realm.kotlin.where
@ -16,6 +15,7 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
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.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager
class Top10Fragment : RealmFragment(), RowRepresentableDataSource, RowRepresentableDelegate { class Top10Fragment : RealmFragment(), RowRepresentableDataSource, RowRepresentableDelegate {
@ -98,10 +98,11 @@ class Top10Fragment : RealmFragment(), RowRepresentableDataSource, RowRepresenta
} }
}) })
// val viewManager = SmoothScrollLinearLayoutManager(requireContext())
val viewManager = SmoothScrollLinearLayoutManager(requireContext())
recyclerView.apply { recyclerView.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = LinearLayoutManager(requireContext()) layoutManager = viewManager
} }
} }

@ -1,23 +1,23 @@
package net.pokeranalytics.android.ui.fragment.components package net.pokeranalytics.android.ui.fragment.components
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.play.core.review.ReviewManagerFactory
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment
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.util.CrashLogging
import timber.log.Timber
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList
abstract class BaseFragment : Fragment() { abstract class BaseFragment : Fragment() {
@ -41,12 +41,6 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
v.setPadding(0, statusBarHeight, 0, 0)
insets
}
} }
override fun onResume() { override fun onResume() {
@ -69,24 +63,11 @@ abstract class BaseFragment : Fragment() {
CrashLogging.log("$this.localClassName onActivityCreated") CrashLogging.log("$this.localClassName onActivityCreated")
} }
val paApplication: PokerAnalyticsApplication?
get() { return (this.activity as? BaseActivity)?.paApplication }
/** /**
* Method called when the activity override onBackPressed and send the information to the fragment * Method called when the activity override onBackPressed and send the information to the fragment
*/ */
open fun onBackPressed() {} open fun onBackPressed() {}
/**
* Method called when the HomeActivity has resumed, spread to tabs only
*/
open fun activityResumed() {}
/**
* Method called when a HomeActivity tab has been selected
*/
open fun selectedTab() {}
// /** // /**
// * Ask for app permission // * Ask for app permission
// */ // */
@ -181,13 +162,13 @@ abstract class BaseFragment : Fragment() {
alternativeLabels) alternativeLabels)
} }
fun showSnackBar(message: String) { /***
this.view?.let { view -> * Returns whether the network is available or not
val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE) */
snackBar.show() fun isNetworkAvailable(): Boolean {
} ?: run { val cm = requireContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
Timber.d("No parent view for snackbar") val capability = cm.getNetworkCapabilities(cm.activeNetwork)
} return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
} }
} }

@ -4,7 +4,6 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.ImageView
@ -28,7 +27,8 @@ import net.pokeranalytics.android.util.Preferences
* - Listen for INTENT_FILTER_UPDATE_FILTER_UI * - Listen for INTENT_FILTER_UPDATE_FILTER_UI
* - * -
*/ */
open class FilterableFragment : RealmFragment(), FilterHandler { open class FilterableFragment : RealmFragment(),
FilterHandler {
override var currentFilterable: FilterableType = FilterableType.ALL override var currentFilterable: FilterableType = FilterableType.ALL
set(value) { set(value) {
@ -58,12 +58,11 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
parentActivity?.registerReceiver(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { updateFilterUIBroadcast, IntentFilter(
parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI), Context.RECEIVER_EXPORTED) INTENT_FILTER_UPDATE_FILTER_UI
} else { )
parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI)) )
}
} }
override fun onDestroy() { override fun onDestroy() {
@ -84,7 +83,7 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar -> view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
toolbar.menu.removeItem(R.id.menu_item_filter) toolbar.menu.removeItem(R.id.menu_item_filter)
filterMenuItem = toolbar.menu?.add(0, R.id.menu_item_filter, 10, R.string.filter) filterMenuItem = toolbar.menu?.add(0, R.id.menu_item_filter, 0, R.string.filter)
filterMenuItem?.setIcon(R.drawable.ic_outline_filter_list) filterMenuItem?.setIcon(R.drawable.ic_outline_filter_list)
filterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM) filterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} }

@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
@ -78,6 +76,18 @@ open class RealmFragment : BaseFragment() {
this.observedRealmResults.add(results) this.observedRealmResults.add(results)
} }
// fun listenRealmChanges(listener: RealmAsyncListener, clazz: Class<out RealmModel>) {
//
// this.changeListener = listener
//
// this.realmResults = this.realm.where(clazz).findAllAsync()
// this.realmResults?.addChangeListener { t, _ ->
// Timber.d("Realm changes: ${realmResults?.size}, $this")
// this.changeListener?.asyncListenedEntityChange(t.realm)
// }
//
// }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()

@ -112,10 +112,10 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
private fun initModel() { private fun initModel() {
val row = config?.row val row = config?.row
?: (activity as? BaseActivity)?.bottomSheetViewModel?.rowRepresentable ?: (requireActivity() as? BaseActivity)?.bottomSheetViewModel?.rowRepresentable
?: throw PAIllegalStateException("row not found") ?: throw PAIllegalStateException("row not found")
val delegate = config?.delegate val delegate = config?.delegate
?: (activity as? BaseActivity)?.bottomSheetViewModel?.delegate ?: (requireActivity() as? BaseActivity)?.bottomSheetViewModel?.delegate
?: throw PAIllegalStateException("delegate not found") ?: throw PAIllegalStateException("delegate not found")
val factory = BottomSheetViewModelFactory(row, delegate) val factory = BottomSheetViewModelFactory(row, delegate)
@ -216,7 +216,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
bottomSheetToolbar.menu.findItem(R.id.actionAdd).setOnMenuItemClickListener { bottomSheetToolbar.menu.findItem(R.id.actionAdd).setOnMenuItemClickListener {
val liveData = when (row) { val liveData = when (row) {
SessionPropertiesRow.GAME -> LiveData.GAME SessionPropertiesRow.GAME -> LiveData.GAME
SessionPropertiesRow.BANKROLL, TransactionPropertiesRow.BANKROLL, TransactionPropertiesRow.DESTINATION -> LiveData.BANKROLL SessionPropertiesRow.BANKROLL, TransactionPropertiesRow.BANKROLL -> LiveData.BANKROLL
SessionPropertiesRow.LOCATION -> LiveData.LOCATION SessionPropertiesRow.LOCATION -> LiveData.LOCATION
SessionPropertiesRow.TOURNAMENT_NAME -> LiveData.TOURNAMENT_NAME SessionPropertiesRow.TOURNAMENT_NAME -> LiveData.TOURNAMENT_NAME
SessionPropertiesRow.TOURNAMENT_FEATURE -> LiveData.TOURNAMENT_FEATURE SessionPropertiesRow.TOURNAMENT_FEATURE -> LiveData.TOURNAMENT_FEATURE

@ -10,6 +10,7 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import kotlinx.android.synthetic.main.view_keyboard_stakes.view.*
import net.pokeranalytics.android.databinding.BottomSheetStakesBinding import net.pokeranalytics.android.databinding.BottomSheetStakesBinding
import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException
import java.text.NumberFormat import java.text.NumberFormat
@ -109,7 +110,7 @@ class BottomSheetStakesFragment : BottomSheetFragment() {
this.focusEditTextAndHideKeyboard(binding.blindsEditText) this.focusEditTextAndHideKeyboard(binding.blindsEditText)
// binding.stakesKeyboard.visibility = View.VISIBLE // binding.stakesKeyboard.visibility = View.VISIBLE
binding.stakesKeyboard.setSeparatorVisibility(true) binding.stakesKeyboard.value_separator.visibility = View.VISIBLE
return@setOnTouchListener true return@setOnTouchListener true
@ -121,9 +122,7 @@ class BottomSheetStakesFragment : BottomSheetFragment() {
this.focusEditTextAndHideKeyboard(binding.anteEditText) this.focusEditTextAndHideKeyboard(binding.anteEditText)
binding.stakesKeyboard.setSeparatorVisibility(false) binding.stakesKeyboard.value_separator.visibility = View.GONE
// binding.stakesKeyboard.value_separator.visibility = View.GONE
// binding.stakesKeyboard.visibility = View.VISIBLE // binding.stakesKeyboard.visibility = View.VISIBLE
// binding.stakesKeyboard.visibility = View.GONE // binding.stakesKeyboard.visibility = View.GONE

@ -3,9 +3,11 @@ package net.pokeranalytics.android.ui.fragment.report
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Report import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
@ -58,17 +60,17 @@ abstract class AbstractReportFragment : DataManagerFragment() {
override fun saveData() { override fun saveData() {
activity?.let { activity -> activity?.let {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(it)
// Get the layout inflater // Get the layout inflater
val inflater = requireActivity().layoutInflater val inflater = requireActivity().layoutInflater
// Inflate and set the layout for the dialog // Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout // Pass null as the parent view because its going in the dialog layout
val view = inflater.inflate(R.layout.dialog_edit_text, null) val view = inflater.inflate(net.pokeranalytics.android.R.layout.dialog_edit_text, null)
val nameEditText = val nameEditText =
view.findViewById<EditText>(R.id.reportName) view.findViewById<EditText>(net.pokeranalytics.android.R.id.reportName)
nameEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES nameEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
this.model.primaryKey?.let { id -> this.model.primaryKey?.let { id ->
@ -79,7 +81,7 @@ abstract class AbstractReportFragment : DataManagerFragment() {
builder.setView(view) builder.setView(view)
// Add action buttons // Add action buttons
.setPositiveButton(R.string.save) { dialog, _ -> .setPositiveButton(net.pokeranalytics.android.R.string.save) { dialog, _ ->
try { try {
saveReport(nameEditText.text.toString()) saveReport(nameEditText.text.toString())
dialog.dismiss() dialog.dismiss()
@ -87,11 +89,24 @@ abstract class AbstractReportFragment : DataManagerFragment() {
Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show()
} }
} }
.setNegativeButton(R.string.cancel) { dialog, _ -> .setNegativeButton(net.pokeranalytics.android.R.string.cancel) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
val dialog = builder.create() val dialog = builder.create()
dialog.setOnShowListener {
nameEditText.requestFocus()
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
dialog.setOnDismissListener {
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
dialog.show() dialog.show()
} ?: throw PAIllegalStateException("Activity cannot be null") } ?: throw PAIllegalStateException("Activity cannot be null")

@ -5,8 +5,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
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.CoroutineScope
@ -14,13 +12,14 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.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.model.realm.ComputableResult
import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.adapter.DisplayDescriptor import net.pokeranalytics.android.ui.adapter.DisplayDescriptor
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
@ -84,11 +83,6 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
initData() initData()
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
report?.let { report?.let {
showResults() showResults()
} }
@ -163,17 +157,25 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
private fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> { private fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
val rows: ArrayList<RowRepresentable> = ArrayList() val rows: ArrayList<RowRepresentable> = ArrayList()
this.context?.let { context ->
report.results.forEach { result -> report.results.forEach { result ->
val title = result.group.query.getName(context).capitalize() val title = result.group.query.getName(requireContext()).capitalize()
rows.add(CustomizableRowRepresentable(title = title)) rows.add(CustomizableRowRepresentable(title = title))
val statList = result.group.displayedStats ?: report.options.stats val statList = result.group.displayedStats ?: report.options.stats
statList.forEach { stat -> statList.forEach { stat ->
rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(context))) rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(requireContext())))
}
} }
} }
return rows return rows
// val rows: ArrayList<RowRepresentable> = ArrayList()
// report.options.stats.forEach {stat ->
// rows.add(CustomizableRowRepresentable(title = stat.localizedTitle(requireContext())))
// report.results.forEach {
// val title = it.group.name
// rows.add(StatRow(stat, it.computedStat(stat), it.group.name, title))
// }
// }
// return rows
} }
// RowRepresentableDelegate // RowRepresentableDelegate
@ -215,7 +217,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
var report: Report? = null var report: Report? = null
val test = GlobalScope.async { val test = GlobalScope.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()

@ -15,11 +15,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope 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.calcul.*
import net.pokeranalytics.android.calculus.AggregationType import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
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.*
import net.pokeranalytics.android.databinding.FragmentProgressReportBinding import net.pokeranalytics.android.databinding.FragmentProgressReportBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.combined import net.pokeranalytics.android.model.combined
@ -82,6 +82,12 @@ class ProgressReportFragment : AbstractReportFragment() {
override fun onDestroyView() { override fun onDestroyView() {
// childFragmentManager.findFragmentByTag(GRAPH_TAG)?.let { fragment ->
// val fragmentTransaction = childFragmentManager.beginTransaction()
// fragmentTransaction.remove(fragment)
// fragmentTransaction.commit()
// }
AppReviewManager.requestReview() AppReviewManager.requestReview()
super.onDestroyView() super.onDestroyView()
@ -166,11 +172,12 @@ class ProgressReportFragment : AbstractReportFragment() {
GlobalScope.launch { GlobalScope.launch {
val s = Date() val s = Date()
// Timber.d(">>> start...") Timber.d(">>> start...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
selectedReport.results.firstOrNull()?.group?.let { group -> val group = selectedReport.results.first().group
val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType) val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType)
reports[aggregationType] = report reports[aggregationType] = report
@ -186,8 +193,6 @@ class ProgressReportFragment : AbstractReportFragment() {
graphContainer.showWithAnimation() graphContainer.showWithAnimation()
} }
} }
}
} }
/** /**

@ -17,9 +17,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope 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.calcul.defaultStatEntries
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.defaultStatEntries
import net.pokeranalytics.android.databinding.FragmentCalendarDetailsBinding import net.pokeranalytics.android.databinding.FragmentCalendarDetailsBinding
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
@ -32,10 +32,11 @@ import net.pokeranalytics.android.ui.helpers.AppReviewManager
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.DualStatRow
import net.pokeranalytics.android.ui.view.rows.GraphRow import net.pokeranalytics.android.ui.view.rows.GraphRow
import net.pokeranalytics.android.ui.view.rows.DualStatRow
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
@ -177,7 +178,6 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource
when (model.sessionTypeCondition) { when (model.sessionTypeCondition) {
QueryCondition.IsCash -> query.add(QueryCondition.IsCash) QueryCondition.IsCash -> query.add(QueryCondition.IsCash)
QueryCondition.IsTournament -> query.add(QueryCondition.IsTournament) QueryCondition.IsTournament -> query.add(QueryCondition.IsTournament)
else -> {}
} }
val requiredStats: List<Stat> = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY) val requiredStats: List<Stat> = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY)
@ -215,7 +215,7 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource
stat = Stat.STANDARD_DEVIATION stat = Stat.STANDARD_DEVIATION
) )
) )
rowRepresentables.add(DualStatRow(it.computedStat(Stat.WIN_RATIO), it.computedStat(Stat.MAXIMUM_NET_RESULT))) rowRepresentables.add(DualStatRow(it.computedStat(Stat.WIN_RATIO), it.computedStat(Stat.MAXIMUM_NETRESULT)))
rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.volume)) rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.volume))
rowRepresentables.add(GraphRow(durationDataSet, report = report, stat = Stat.HOURLY_DURATION)) rowRepresentables.add(GraphRow(durationDataSet, report = report, stat = Stat.HOURLY_DURATION))
rowRepresentables.add(DualStatRow(it.computedStat(Stat.HOURLY_DURATION), it.computedStat(Stat.AVERAGE_HOURLY_DURATION))) rowRepresentables.add(DualStatRow(it.computedStat(Stat.HOURLY_DURATION), it.computedStat(Stat.AVERAGE_HOURLY_DURATION)))

@ -1,10 +1,7 @@
package net.pokeranalytics.android.ui.modules.calendar package net.pokeranalytics.android.ui.modules.calendar
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import io.realm.Realm import io.realm.Realm
@ -24,7 +21,6 @@ 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.ComputableResult
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.UserConfig 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
@ -33,7 +29,6 @@ import net.pokeranalytics.android.ui.extensions.hideWithAnimation
import net.pokeranalytics.android.ui.extensions.showWithAnimation import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.components.RealmAsyncListener import net.pokeranalytics.android.ui.fragment.components.RealmAsyncListener
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity
import net.pokeranalytics.android.ui.view.CalendarTabs import net.pokeranalytics.android.ui.view.CalendarTabs
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
@ -41,7 +36,6 @@ 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 timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.set
class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentableDataSource, class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentableDataSource,
@ -95,6 +89,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
setHasOptionsMenu(true)
_binding = FragmentCalendarBinding.inflate(inflater, container, false) _binding = FragmentCalendarBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -106,47 +101,19 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
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)
} }
private var transactionFilterMenuItem: MenuItem? = null
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu.clear()
inflater.inflate(R.menu.toolbar_calendar, menu)
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
toolbar.menu.removeItem(R.id.menu_item_transaction_filter)
transactionFilterMenuItem = toolbar.menu?.add(0, R.id.menu_item_transaction_filter, 1, R.string.filter)
transactionFilterMenuItem?.setIcon(R.drawable.baseline_payment_24)
transactionFilterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
setTransactionFilterItemColor()
}
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_item_transaction_filter -> showTransactionFilter() R.id.grid -> showGridCalendar()
}
return super.onOptionsItemSelected(item)
}
private fun setTransactionFilterItemColor() {
context?.let {
val userConfig = UserConfig.getConfiguration(getRealm())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.let { item ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
item.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
}
}
}
private fun showTransactionFilter() {
context?.let {
TransactionFilterActivity.newInstance(it)
} }
return true
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -213,6 +180,9 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java) override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java)
// override fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {
// launchAsyncStatComputation()
// }
// Business // Business
@ -313,8 +283,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
layoutManager = viewManager layoutManager = viewManager
adapter = calendarAdapter adapter = calendarAdapter
} }
setTransactionFilterItemColor()
} }
private fun selectTimeFilter(timeFilter: TimeFilter, isChecked: Boolean) { private fun selectTimeFilter(timeFilter: TimeFilter, isChecked: Boolean) {
@ -378,14 +346,11 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Stat.STANDARD_DEVIATION_HOURLY Stat.STANDARD_DEVIATION_HOURLY
) )
val transactionTypes = UserConfig.getConfiguration(realm).transactionTypes(realm)
// All // All
val allOptions = Calculator.Options( val allOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD, progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats, stats = requiredStats,
query = Query(this.sessionTypeCondition), query = Query(this.sessionTypeCondition)
includedTransactions = transactionTypes
) )
val allReport = Calculator.computeStats(realm, options = allOptions) val allReport = Calculator.computeStats(realm, options = allOptions)
this.allComputedResults = allReport.results.first() this.allComputedResults = allReport.results.first()
@ -394,8 +359,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val smOptions = Calculator.Options( val smOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD, progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats, stats = requiredStats,
query = Query(this.slidingMonthQueryCondition, this.sessionTypeCondition), query = Query(this.slidingMonthQueryCondition, this.sessionTypeCondition)
includedTransactions = transactionTypes
) )
val smReport = Calculator.computeStats(realm, options = smOptions) val smReport = Calculator.computeStats(realm, options = smOptions)
@ -414,8 +378,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val options = Calculator.Options( val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD, progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats, stats = requiredStats,
query = query, query = query
includedTransactions = transactionTypes
) )
val report = Calculator.computeStats(realm, options = options) val report = Calculator.computeStats(realm, options = options)
report.results.forEach { computedResults -> report.results.forEach { computedResults ->
@ -431,7 +394,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Calendar.MONTH, Calendar.MONTH,
condition.listOfValues.first() condition.listOfValues.first()
) )
else -> {}
} }
} }
@ -444,8 +406,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val syOptions = Calculator.Options( val syOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD, progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats, stats = requiredStats,
query = Query(this.slidingYearQueryCondition, this.sessionTypeCondition), query = Query(this.slidingYearQueryCondition, this.sessionTypeCondition)
includedTransactions = transactionTypes
) )
val syReport = Calculator.computeStats(realm, options = syOptions) val syReport = Calculator.computeStats(realm, options = syOptions)
@ -466,8 +427,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val options = Calculator.Options( val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD, progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats, stats = requiredStats,
query = query, query = query
includedTransactions = transactionTypes
) )
val report = Calculator.computeStats(realm, options = options) val report = Calculator.computeStats(realm, options = options)
report.results.forEach { computedResults -> report.results.forEach { computedResults ->
@ -479,7 +439,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Calendar.YEAR, Calendar.YEAR,
condition.listOfValues.first() condition.listOfValues.first()
) )
else -> {}
} }
} }
yearlyReports[calendar.time] = computedResults yearlyReports[calendar.time] = computedResults
@ -492,6 +451,18 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms") Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms")
// Logs
/*
Timber.d("========== AnyYear x AnyMonthOfYear")
sortedMonthlyReports.keys.forEach {
Timber.d("$it => ${sortedMonthlyReports[it]?.computedStat(Stat.NET_RESULT)?.value}")
}
Timber.d("========== YEARLY")
sortedYearlyReports.keys.forEach {
Timber.d("$it => ${sortedYearlyReports[it]?.computedStat(Stat.NET_RESULT)?.value}")
}
*/
} }
/** /**
@ -605,10 +576,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
} }
override fun asyncListenedEntityChange(realm: Realm) { override fun asyncListenedEntityChange(realm: Realm) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchAsyncStatComputation() launchAsyncStatComputation()
setTransactionFilterItemColor()
}
} }
private fun showGridCalendar() { private fun showGridCalendar() {

@ -8,14 +8,13 @@ import android.view.View
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.api.CurrencyConverterApi import net.pokeranalytics.android.api.FreeConverterApi
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.ResultCaptureType import net.pokeranalytics.android.model.realm.ResultCaptureType
import net.pokeranalytics.android.ui.activity.CurrenciesActivity import net.pokeranalytics.android.ui.activity.CurrenciesActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.ui.fragment.CurrenciesFragment import net.pokeranalytics.android.ui.fragment.CurrenciesFragment
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
@ -85,8 +84,6 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
onRowValueChanged(currencyCode, BankrollPropertiesRow.CURRENCY) onRowValueChanged(currencyCode, BankrollPropertiesRow.CURRENCY)
if (shouldShowCurrencyRate) { if (shouldShowCurrencyRate) {
refreshRate() refreshRate()
} else {
this.bankroll.currency?.rate = 1.0
} }
} }
} }
@ -140,7 +137,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence { override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> bankroll.name.ifEmpty { NULL_TEXT } SimpleRow.NAME -> if (bankroll.name.isNotEmpty()) bankroll.name else NULL_TEXT
BankrollPropertiesRow.CURRENCY -> { BankrollPropertiesRow.CURRENCY -> {
bankroll.currency?.code?.let { code -> bankroll.currency?.code?.let { code ->
Currency.getInstance(code).currencyCode Currency.getInstance(code).currencyCode
@ -266,25 +263,13 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
} }
this.lastRefreshRateCall = System.currentTimeMillis() this.lastRefreshRateCall = System.currentTimeMillis()
// val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}" val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}"
bankroll.currency?.code?.let { from ->
val to = defaultCurrency.currencyCode
CurrencyConverterApi.currencyRate(from, to, requireContext()) { rate, error ->
rate?.let { FreeConverterApi.currencyRate(currenciesConverterValue, requireContext()) { rate ->
onRowValueChanged(rate, BankrollPropertiesRow.RATE) onRowValueChanged(rate, BankrollPropertiesRow.RATE)
}
error?.localizedMessage?.let { message ->
toast(message)
}
// onRowValueChanged(rate, BankrollPropertiesRow.RATE)
isRefreshingRate = false isRefreshingRate = false
rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE) rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE)
} }
}
this.isRefreshingRate = true this.isRefreshingRate = true
this.rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE) this.rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE)
@ -295,6 +280,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
this.bankrollModel.selectedCaptureType.value?.let { this.bankrollModel.selectedCaptureType.value?.let {
Preferences.setResultCaptureType(this.bankroll, it, requireContext()) Preferences.setResultCaptureType(this.bankroll, it, requireContext())
} }
} }
} }

@ -211,9 +211,9 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa
*/ */
private fun initUI() { private fun initUI() {
// val addItem = binding.addItem val addItem = binding.addItem
// val sortChoices = binding.sortChoices val sortChoices = binding.sortChoices
// val sortDescending = binding.sortDescending val sortDescending = binding.sortDescending
val recyclerView = binding.recyclerView val recyclerView = binding.recyclerView
customField.updateRowRepresentation() customField.updateRowRepresentation()

@ -8,6 +8,7 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.ui.activity.components.MediaActivity import net.pokeranalytics.android.ui.activity.components.MediaActivity
import java.io.File import java.io.File
import java.util.*
class EditableDataActivity : MediaActivity() { class EditableDataActivity : MediaActivity() {
@ -51,11 +52,6 @@ class EditableDataActivity : MediaActivity() {
initUI() initUI()
} }
// override fun onPause() {
// super.onPause()
// this.paApplication.backupOperator?.backupIfNecessary()
// }
/** /**
* Init UI * Init UI
*/ */

@ -9,9 +9,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -21,35 +18,23 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentPlayerBinding import net.pokeranalytics.android.databinding.FragmentPlayerBinding
import net.pokeranalytics.android.model.realm.Comment import net.pokeranalytics.android.model.realm.Comment
import net.pokeranalytics.android.model.realm.Player import net.pokeranalytics.android.model.realm.Player
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.activity.ColorPickerActivity import net.pokeranalytics.android.ui.activity.ColorPickerActivity
import net.pokeranalytics.android.ui.activity.components.CameraActivity
import net.pokeranalytics.android.ui.activity.components.MediaActivity import net.pokeranalytics.android.ui.activity.components.MediaActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.showAlertDialog import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
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.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import timber.log.Timber
import java.io.File import java.io.File
/** /**
* Player data fragment * Player data fragment
*/ */
class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource {
private val playerModel: PlayerDataViewModel
get() { return this.model as PlayerDataViewModel }
override val modelClass: Class<out DataManagerViewModel> = PlayerDataViewModel::class.java
companion object { companion object {
const val REQUEST_CODE_PICK_COLOR = 1000 const val REQUEST_CODE_PICK_COLOR = 1000
} }
@ -79,13 +64,6 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
player.color = if (color != Color.TRANSPARENT) color else null player.color = if (color != Color.TRANSPARENT) color else null
rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE) rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE)
} }
if (requestCode == RequestCode.CAMERA.value) {
val uri = data?.getStringExtra(CameraActivity.IMAGE_URI)
this.player.picture = uri
rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE)
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -98,38 +76,23 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
_binding = null _binding = null
} }
var pickVisualMediaRequest: ActivityResultLauncher<PickVisualMediaRequest>? = null
/** /**
* Init UI * Init UI
*/ */
private fun initUI() { private fun initUI() {
mediaActivity = parentActivity as MediaActivity? mediaActivity = parentActivity as MediaActivity?
this.playerModel.updateRowRepresentation() player.updateRowRepresentation()
if (!deleteButtonShouldAppear) { if (!deleteButtonShouldAppear) {
onRowSelected(0, PlayerPropertiesRow.NAME) onRowSelected(0, PlayerPropertiesRow.NAME)
} }
binding.addComment.setOnClickListener { binding.addComment.setOnClickListener {
val comment = this.playerModel.addComment() val comment = player.addComment()
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
onRowSelected(-1, comment) onRowSelected(-1, comment)
} }
this.pickVisualMediaRequest = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
this.player.picture = uri.toString()
if (uri != null) {
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
context?.contentResolver?.takePersistableUriPermission(uri, flag)
Timber.d("Selected URI: $uri")
} else {
Timber.d("No media selected")
}
rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE)
}
} }
override fun getPhotos(files: ArrayList<File>) { override fun getPhotos(files: ArrayList<File>) {
@ -144,8 +107,8 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
return this return this
} }
override fun adapterRows(): List<RowRepresentable> { override fun adapterRows(): List<RowRepresentable>? {
return this.playerModel.adapterRows() return player.adapterRows()
} }
override fun viewTypeForPosition(position: Int): Int { override fun viewTypeForPosition(position: Int): Int {
@ -168,8 +131,8 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
PlayerPropertiesRow.NAME -> player.name.ifEmpty { NULL_TEXT } PlayerPropertiesRow.NAME -> if (player.name.isNotEmpty()) player.name else NULL_TEXT
PlayerPropertiesRow.SUMMARY -> player.summary.ifEmpty { NULL_TEXT } PlayerPropertiesRow.SUMMARY -> if (player.summary.isNotEmpty()) player.summary else NULL_TEXT
else -> super.charSequenceForRow(row, context, 0) else -> super.charSequenceForRow(row, context, 0)
} }
} }
@ -181,9 +144,6 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
val data = arrayListOf(RowRepresentableEditDescriptor(row.content)) val data = arrayListOf(RowRepresentableEditDescriptor(row.content))
showBottomSheet(row, this, data, isClearable = false, isDeletable = true) showBottomSheet(row, this, data, isClearable = false, isDeletable = true)
} }
is HandHistory -> {
HandHistoryActivity.newInstance(this, row.id)
}
else -> super.onRowSelected(position, row, tag) else -> super.onRowSelected(position, row, tag)
} }
} }
@ -192,19 +152,14 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
when (row) { when (row) {
is Comment -> { is Comment -> {
row.updateValue(value, row) row.updateValue(value, row)
this.playerModel.updateRowRepresentation() player.updateRowRepresentation()
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
} }
else -> { else -> {
super.onRowValueChanged(value, row) super.onRowValueChanged(value, row)
when (row) { if (row == PlayerPropertiesRow.NAME) {
PlayerPropertiesRow.NAME -> {
rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE) rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE)
}
PlayerPropertiesRow.TAB_SELECTOR -> {
this.playerModel.selectedTab = value as Int
rowRepresentableAdapter.notifyDataSetChanged()
}
} }
} }
} }
@ -217,8 +172,8 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
if (row.isValidForDelete(getRealm())) { if (row.isValidForDelete(getRealm())) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
delay(300) delay(300)
showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = { showAlertDialog(requireContext(), message = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = {
playerModel.deleteComment(row) player.deleteComment(row)
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
}) })
} }
@ -247,15 +202,8 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
builder.setItems(placesArray.toTypedArray()) { _, which -> builder.setItems(placesArray.toTypedArray()) { _, which ->
when (placesArray[which]) { when (placesArray[which]) {
getString(R.string.take_a_picture) -> { getString(R.string.take_a_picture) -> mediaActivity?.openImageCaptureIntent(false)
CameraActivity.newInstanceForResult(this, RequestCode.CAMERA) getString(R.string.library) -> mediaActivity?.openImageGalleryIntent(false)
// mediaActivity?.takePicture()
} // mediaActivity?.openImageCaptureIntent(false)
getString(R.string.library) -> {
pickVisualMediaRequest?.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
// mediaActivity?.openImageGalleryIntent(false)
getString(R.string.select_a_color) -> { getString(R.string.select_a_color) -> {
ColorPickerActivity.newInstanceForResult(this, REQUEST_CODE_PICK_COLOR) ColorPickerActivity.newInstanceForResult(this, REQUEST_CODE_PICK_COLOR)
} }
@ -270,7 +218,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
override fun onDataSaved() { override fun onDataSaved() {
super.onDataSaved() super.onDataSaved()
this.playerModel.cleanupComments() player.cleanupComments()
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {

@ -1,153 +0,0 @@
package net.pokeranalytics.android.ui.modules.data
import android.content.Context
import io.realm.Realm
import io.realm.kotlin.where
import net.pokeranalytics.android.model.realm.Comment
import net.pokeranalytics.android.model.realm.Player
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.isSameDay
import net.pokeranalytics.android.util.extensions.mediumDate
import java.util.*
class PlayerDataViewModel : DataManagerViewModel(), StaticRowRepresentableDataSource {
private var rowRepresentation: List<RowRepresentable> = mutableListOf()
private var commentsToDelete: ArrayList<Comment> = ArrayList()
var selectedTab: Int = 0
set(value) {
field = value
this.updateRowRepresentation()
}
private val player: Player
get() {
return this.item as Player
}
override fun adapterRows(): List<RowRepresentable> {
return this.rowRepresentation
}
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
PlayerPropertiesRow.NAME -> this.player.name.ifEmpty { NULL_TEXT }
else -> return super.charSequenceForRow(row, context, 0)
}
}
fun updateRowRepresentation() {
this.rowRepresentation = this.updatedRowRepresentationForCurrentState()
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(PlayerPropertiesRow.IMAGE)
rows.add(PlayerPropertiesRow.NAME)
rows.add(PlayerPropertiesRow.SUMMARY)
val realm = Realm.getDefaultInstance()
val hands = this.player.hands(realm)
realm.close()
if (this.player.comments.isNotEmpty() || hands.isNotEmpty()) {
rows.add(PlayerPropertiesRow.TAB_SELECTOR)
}
when(this.selectedTab) {
0 -> this.addCommentSection(rows)
1 -> rows.addAll(hands)
}
return rows
}
private fun addCommentSection(rows: ArrayList<RowRepresentable>) {
if (this.player.comments.size > 0) {
val currentCommentCalendar = Calendar.getInstance()
val currentDateCalendar = Calendar.getInstance()
val commentsToDisplay = ArrayList<Comment>()
commentsToDisplay.addAll(this.player.comments)
commentsToDisplay.sortByDescending { it.date }
commentsToDisplay.forEachIndexed { index, comment ->
currentCommentCalendar.time = comment.date
if (!currentCommentCalendar.isSameDay(currentDateCalendar) || index == 0) {
currentDateCalendar.time = currentCommentCalendar.time
// Adds day sub section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_SUBTITLE, title = currentDateCalendar.time.mediumDate()))
}
// Adds comment
rows.add(comment)
}
rows.add(SeparatorRow())
}
}
/**
* Add an entry
*/
fun addComment(): Comment {
val entry = Comment()
this.player.comments.add(entry)
updateRowRepresentation()
return entry
}
/**
* Delete an entry
*/
fun deleteComment(comment: Comment) {
commentsToDelete.add(comment)
this.player.comments.remove(comment)
updateRowRepresentation()
}
/**
* Clean up deleted entries
*/
fun cleanupComments() { // called when saving the custom field
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
this.commentsToDelete.forEach { // entries are out of realm
realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
}
}
realm.close()
this.commentsToDelete.clear()
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
when (row) {
PlayerPropertiesRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.player.name))
PlayerPropertiesRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.player.summary))
}
return null
}
}

@ -68,8 +68,6 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
TransactionPropertiesRow.AMOUNT -> if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else NULL_TEXT TransactionPropertiesRow.AMOUNT -> if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else NULL_TEXT
TransactionPropertiesRow.COMMENT -> if (this.transaction.comment.isNotEmpty()) this.transaction.comment else NULL_TEXT TransactionPropertiesRow.COMMENT -> if (this.transaction.comment.isNotEmpty()) this.transaction.comment else NULL_TEXT
TransactionPropertiesRow.DATE -> this.transaction.date.shortDate() TransactionPropertiesRow.DATE -> this.transaction.date.shortDate()
TransactionPropertiesRow.DESTINATION -> this.transaction.destination?.name ?: NULL_TEXT
TransactionPropertiesRow.RATE -> this.transaction.transferRate?.round() ?: NULL_TEXT
else -> super.charSequenceForRow(row, context, 0) else -> super.charSequenceForRow(row, context, 0)
} }
} }
@ -79,7 +77,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
TransactionPropertiesRow.BANKROLL -> row.editingDescriptors( TransactionPropertiesRow.BANKROLL -> row.editingDescriptors(
mapOf( mapOf(
"defaultValue" to this.transaction.bankroll, "defaultValue" to this.transaction.bankroll,
"data" to getRealm().sorted<Bankroll>(omitId = this.transaction.destination?.id) "data" to getRealm().sorted<Bankroll>()
) )
) )
TransactionPropertiesRow.TYPE -> row.editingDescriptors( TransactionPropertiesRow.TYPE -> row.editingDescriptors(
@ -90,13 +88,6 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
) )
TransactionPropertiesRow.AMOUNT -> row.editingDescriptors(mapOf("defaultValue" to (if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else ""))) TransactionPropertiesRow.AMOUNT -> row.editingDescriptors(mapOf("defaultValue" to (if (this.transaction.amount != 0.0) abs(this.transaction.amount).round() else "")))
TransactionPropertiesRow.COMMENT -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.comment)) TransactionPropertiesRow.COMMENT -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.comment))
TransactionPropertiesRow.DESTINATION -> row.editingDescriptors(
mapOf(
"defaultValue" to this.transaction.destination,
"data" to getRealm().sorted<Bankroll>(omitId = this.transaction.bankroll?.id)
)
)
TransactionPropertiesRow.RATE -> row.editingDescriptors(mapOf("defaultValue" to this.transaction.transferRate))
else -> super.editDescriptors(row) else -> super.editDescriptors(row)
} }
} }
@ -117,13 +108,6 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
super.onRowValueChanged(value, row) super.onRowValueChanged(value, row)
when (row) {
TransactionPropertiesRow.TYPE, TransactionPropertiesRow.DESTINATION -> {
this.rowRepresentableAdapter.notifyDataSetChanged()
}
}
this.rowRepresentableAdapter.refreshRow(row) this.rowRepresentableAdapter.refreshRow(row)
this.selectNextRow(row) this.selectNextRow(row)
} }
@ -134,19 +118,13 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
private fun selectNextRow(currentRow: RowRepresentable) { private fun selectNextRow(currentRow: RowRepresentable) {
if (this.model.primaryKey == null) { // automatically change the row for new data if (this.model.primaryKey == null) { // automatically change the row for new data
this.adapterRows()?.let { rows ->
val index = rows.indexOf(currentRow)
if (index + 1 < rows.size) {
when (val next = rows[index + 1]) {
TransactionPropertiesRow.DATE, TransactionPropertiesRow.COMMENT -> {}
else -> {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
delay(200) delay(200)
onRowSelected(0, next) when (currentRow) {
} TransactionPropertiesRow.BANKROLL -> onRowSelected(0, TransactionPropertiesRow.TYPE)
} TransactionPropertiesRow.TYPE -> onRowSelected(0, TransactionPropertiesRow.AMOUNT)
} // TransactionRow.AMOUNT -> onRowSelected(0, TransactionRow.DATE)
// TransactionRow.DATE -> onRowSelected(0, TransactionRow.COMMENT)
} }
} }
} }

@ -8,7 +8,6 @@ import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase 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
@ -255,10 +254,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
binding.messageBox.isVisible = false binding.messageBox.isVisible = false
} }
// val viewManager = SmoothScrollLinearLayoutManager(requireContext()) val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.menuRecyclerView.apply { binding.menuRecyclerView.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = LinearLayoutManager(requireContext()) layoutManager = viewManager
} }
// Add button // Add button
@ -569,7 +568,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
} }
} }
override fun activityResumed() { fun activityResumed() {
this.sessionAdapter.notifyDataSetChanged() // refreshes session durations this.sessionAdapter.notifyDataSetChanged() // refreshes session durations
} }
@ -583,31 +582,33 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
private fun retrieveLatestBlogPosts() { private fun retrieveLatestBlogPosts() {
this.context?.let { context ->
val now = Date().time val now = Date().time
if (Preferences.shouldShowBlogTips(context) && Preferences.getLastBlogTipsRetrievalDate(context) + 24 * 3600 * 1000 < now) { // if (true) {
if (Preferences.shouldShowBlogTips(requireContext()) && Preferences.getLastBlogTipsRetrievalDate(requireContext()) + 24 * 3600 * 1000 < now) {
BlogPostApi.getLatestPosts(context) { posts -> BlogPostApi.getLatestPosts(requireContext()) { posts ->
Preferences.setLastBlogTipsRetrievalDate(now, context) context?.let {
Preferences.setLastBlogTipsRetrievalDate(now, requireContext())
var count = 0 var count = 0
if (posts.isNotEmpty()) { if (posts.isNotEmpty()) {
Preferences.setLatestRetrievedBlogPostId(posts.first().id, context) Preferences.setLatestRetrievedBlogPostId(posts.first().id, requireContext())
val id = Preferences.getLatestDisplayedBlogPostId(context)
val id = Preferences.getLatestDisplayedBlogPostId(requireContext())
count = posts.count { it.id > id } count = posts.count { it.id > id }
} }
displayBlogPostButton(count) displayBlogPostButton(count)
} }
} }
} }
} }
private fun displayBlogPostButton(newCount: Int = 0) { private fun displayBlogPostButton(newCount: Int = 0) {
context?.let { context ->
var show = false var show = false
if (Preferences.shouldShowBlogTips(context) && newCount > 0) { if (Preferences.shouldShowBlogTips(requireContext()) && newCount > 0) {
show = true show = true
this.badgeDrawable?.number = newCount this.badgeDrawable?.number = newCount
} }
@ -616,5 +617,3 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
} }
} }
}

@ -117,8 +117,6 @@ class NewDataMenuActivity : BaseActivity() {
private fun showMenu() { private fun showMenu() {
val menuContainer = binding.menuContainer val menuContainer = binding.menuContainer
if (menuContainer.isAttachedToWindow) {
val cx = menuContainer.measuredWidth - fabSize / 2 val cx = menuContainer.measuredWidth - fabSize / 2
val cy = menuContainer.measuredHeight - fabSize / 2 val cy = menuContainer.measuredHeight - fabSize / 2
val finalRadius = max(menuContainer.width, menuContainer.height) val finalRadius = max(menuContainer.width, menuContainer.height)
@ -129,8 +127,6 @@ class NewDataMenuActivity : BaseActivity() {
anim.start() anim.start()
} }
}
/** /**
* Hide menu * Hide menu
*/ */
@ -149,17 +145,11 @@ class NewDataMenuActivity : BaseActivity() {
anim.duration = 150 anim.duration = 150
anim.addListener(object : AnimatorListenerAdapter() { anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
menuContainer.visibility = View.INVISIBLE menuContainer.visibility = View.INVISIBLE
finish() finish()
} }
// override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
// super.onAnimationEnd(animation, isReverse)
// menuContainer.visibility = View.INVISIBLE
// finish()
// }
}) })
anim.start() anim.start()

@ -15,7 +15,7 @@ import timber.log.Timber
class FilterDetailsViewModelFactory(var filter: Filter, private var categoryRow: FilterCategoryRow): ViewModelProvider.Factory { class FilterDetailsViewModelFactory(var filter: Filter, private var categoryRow: FilterCategoryRow): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FilterDetailsViewModel(categoryRow, filter) as T return FilterDetailsViewModel(categoryRow, filter) as T
} }
@ -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
} }

@ -3,10 +3,7 @@ package net.pokeranalytics.android.ui.modules.handhistory.editor
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.text.InputType import android.text.InputType
import android.view.* import android.view.*
import android.widget.Button import android.widget.*
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatButton
import androidx.core.view.isEmpty import androidx.core.view.isEmpty
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -148,7 +145,7 @@ class EditorAdapter(
} }
} }
abstract inner class AbstractRowHandHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { abstract inner class RowHandHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder {
private var currentPosition = 0 private var currentPosition = 0
@ -198,25 +195,25 @@ class EditorAdapter(
// hides soft input view, // hides soft input view,
// 22/06/22: does not seem useful as - I believe - manifest activity has android:windowSoftInputMode="stateAlwaysHidden" // 22/06/22: does not seem useful as - I believe - manifest activity has android:windowSoftInputMode="stateAlwaysHidden"
editText.setTextIsSelectable(true) editText.setTextIsSelectable(true)
//
// // Enabled // Enabled
val isEnabled = adapter.dataSource.isEnabled(row, tag) val isEnabled = adapter.dataSource.isEnabled(row, tag)
editText.isEnabled = isEnabled editText.isEnabled = isEnabled
//
// // Text // Text
val string = adapter.dataSource.charSequenceForRow(row, itemView.context, tag) val string = adapter.dataSource.charSequenceForRow(row, itemView.context, tag)
editText.setText(string) editText.setText(string)
//
// // Focus // Focus
val isFocused = adapter.dataSource.isSelected(position, row, tag) val isFocused = adapter.dataSource.isSelected(position, row, tag)
// toggleFocus(editText, isFocused) toggleFocus(editText, isFocused)
// editText.isFocusable = adapter.dataSource.isFocusable(position, row, tag) editText.isFocusable = adapter.dataSource.isFocusable(position, row, tag)
// editText.isFocusableInTouchMode = adapter.dataSource.isFocusable(position, row, tag) editText.isFocusableInTouchMode = adapter.dataSource.isFocusable(position, row, tag)
//
// // Put cursor at the end // Put cursor at the end
// if (isFocused) { if (isFocused) {
// editText.setSelection(editText.text.length) editText.setSelection(editText.text.length)
// } }
// Background // Background
setViewBackground(editText, isEnabled, isFocused) setViewBackground(editText, isEnabled, isFocused)
@ -245,7 +242,7 @@ class EditorAdapter(
editText.isFocusableInTouchMode = true editText.isFocusableInTouchMode = true
editText.setOnTouchListener { view, event -> editText.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
// Both are required, otherwise requestFocus() fails // Both are required, otherwise requestFocus() fails
@ -256,8 +253,6 @@ class EditorAdapter(
editTextSelected(editText) editTextSelected(editText)
} }
view.performClick()
return@setOnTouchListener true return@setOnTouchListener true
} }
@ -362,7 +357,7 @@ class EditorAdapter(
layout.setOnClickListener { layout.setOnClickListener {
if ((dataSource as EditorViewModel).isEdited) { if ((dataSource as EditorViewModel).isEdited) {
delegate?.onRowSelected(this.currentPosition, row, layout.tag as Int) delegate?.onRowSelected(this.currentPosition, row, layout.tag as Int)
setViewBackground(layout, isEnabled = true, isFocused = true) setViewBackground(layout, true, true)
} }
} }
@ -372,7 +367,7 @@ class EditorAdapter(
} }
inner class RowStraddleHolder(itemView: View) : AbstractRowHandHolder(itemView) { inner class RowStraddleHolder(itemView: View) : RowHandHolder(itemView) {
private var container: ViewGroup = itemView.findViewById(R.id.container) private var container: ViewGroup = itemView.findViewById(R.id.container)
private var positionsChipGroup: FlowLayout = itemView.findViewById(R.id.positionsChipGroup) private var positionsChipGroup: FlowLayout = itemView.findViewById(R.id.positionsChipGroup)
@ -414,7 +409,7 @@ class EditorAdapter(
} }
} }
inner class RowPositionHolder(itemView: View) : AbstractRowHandHolder(itemView) { inner class RowPositionHolder(itemView: View) : RowHandHolder(itemView) {
private var recycler: RecyclerView = itemView.findViewById(R.id.recycler) private var recycler: RecyclerView = itemView.findViewById(R.id.recycler)
private var positionAdapter: PositionAdapter = PositionAdapter() private var positionAdapter: PositionAdapter = PositionAdapter()
@ -450,7 +445,7 @@ class EditorAdapter(
} }
inner class RowActionReadHolder(itemView: View) : AbstractRowHandHolder(itemView) { inner class RowActionReadHolder(itemView: View) : RowHandHolder(itemView) {
private var playerImage: PlayerImageView = itemView.findViewById(R.id.player_image_rhar) private var playerImage: PlayerImageView = itemView.findViewById(R.id.player_image_rhar)
private var stackText: TextView = itemView.findViewById(R.id.stackText) private var stackText: TextView = itemView.findViewById(R.id.stackText)
@ -486,7 +481,7 @@ class EditorAdapter(
/** /**
* Display a hand action * Display a hand action
*/ */
inner class RowActionHolder(itemView: View) : AbstractRowHandHolder(itemView) { inner class RowActionHolder(itemView: View) : RowHandHolder(itemView) {
private var playerImage: PlayerImageView = itemView.findViewById(R.id.player_image_rha) private var playerImage: PlayerImageView = itemView.findViewById(R.id.player_image_rha)
private var actionButton: AppCompatButton = itemView.findViewById(R.id.actionButton) private var actionButton: AppCompatButton = itemView.findViewById(R.id.actionButton)
@ -508,19 +503,18 @@ class EditorAdapter(
amountEditText.inputType = InputType.TYPE_NUMBER_FLAG_DECIMAL amountEditText.inputType = InputType.TYPE_NUMBER_FLAG_DECIMAL
amountEditText.isFocusableInTouchMode = true amountEditText.isFocusableInTouchMode = true
amountEditText.setOnTouchListener { view, event -> amountEditText.setOnTouchListener { _, event ->
// Timber.d("=== event.action = ${event.action}") // Timber.d("=== event.action = ${event.action}")
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
// Both are required, otherwise requestFocus() fails // Both are required, otherwise requestFocus() fails
// amountEditText.isFocusable = true amountEditText.isFocusable = true
// amountEditText.isFocusableInTouchMode = true amountEditText.isFocusableInTouchMode = true
//
// amountEditText.requestFocus() amountEditText.requestFocus()
editTextSelected(amountEditText) editTextSelected(amountEditText)
} }
view.performClick()
return@setOnTouchListener true return@setOnTouchListener true
} }
@ -574,7 +568,7 @@ class EditorAdapter(
/** /**
* Display a hand street * Display a hand street
*/ */
inner class RowStreetHolder(itemView: View) : AbstractRowHandHolder(itemView) { inner class RowStreetHolder(itemView: View) : RowHandHolder(itemView) {
private var cardsLayout: LinearLayout = itemView.findViewById(R.id.cardsLayout) private var cardsLayout: LinearLayout = itemView.findViewById(R.id.cardsLayout)
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
@ -592,7 +586,7 @@ class EditorAdapter(
/** /**
* Display a hand action * Display a hand action
*/ */
inner class RowPlayerSummaryHolder(itemView: View) : AbstractRowHandHolder(itemView) { inner class RowPlayerSummaryHolder(itemView: View) : RowHandHolder(itemView) {
private var hpsPlayerImage: PlayerImageView = itemView.findViewById(R.id.hps_player_image) private var hpsPlayerImage: PlayerImageView = itemView.findViewById(R.id.hps_player_image)
private var handLayout: LinearLayout = itemView.findViewById(R.id.handLayout) private var handLayout: LinearLayout = itemView.findViewById(R.id.handLayout)
@ -619,7 +613,7 @@ class EditorAdapter(
} }
} }
abstract inner class AbstractRowPlayerSetup(itemView: View) : AbstractRowHandHolder(itemView) { abstract inner class AbstractRowPlayerSetup(itemView: View) : RowHandHolder(itemView) {
private var psHandLayout: LinearLayout = itemView.findViewById(R.id.ps_hand_layout) private var psHandLayout: LinearLayout = itemView.findViewById(R.id.ps_hand_layout)
private var positionButton: AppCompatButton = itemView.findViewById(R.id.position_button) private var positionButton: AppCompatButton = itemView.findViewById(R.id.position_button)

@ -317,7 +317,7 @@ class EditorFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListen
val handRow = this.model.rowRepresentableForPosition(selection.index) as? HandHistoryRow val handRow = this.model.rowRepresentableForPosition(selection.index) as? HandHistoryRow
val holder = val holder =
binding.recyclerView.findViewHolderForAdapterPosition(selection.index) as? EditorAdapter.AbstractRowHandHolder binding.recyclerView.findViewHolderForAdapterPosition(selection.index) as? EditorAdapter.RowHandHolder
holder?.let { holder?.let {
val amountEditText = it.editTextForTag(selection.tag) val amountEditText = it.editTextForTag(selection.tag)
this.binding.keyboard.setAmountEditText( this.binding.keyboard.setAmountEditText(

@ -117,8 +117,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
type = if (significant != null) { type = if (significant != null) {
val betAmount = significant.action.amount val betAmount = significant.action.amount
val remainingStack = computedAction.stackBeforeActing val remainingStack = computedAction.stackBeforeActing
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0 if (remainingStack != null && betAmount != null && remainingStack < betAmount) {
if (remainingStack != null && betAmount != null && (committedStack + remainingStack < betAmount)) {
Action.Type.CALL_ALLIN Action.Type.CALL_ALLIN
} else { } else {
Action.Type.RAISE_ALLIN Action.Type.RAISE_ALLIN
@ -131,10 +130,8 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
Action.Type.CALL -> { Action.Type.CALL -> {
getStreetLastSignificantAction(computedAction.street, index - 1)?.let { getStreetLastSignificantAction(computedAction.street, index - 1)?.let {
val betAmount = it.action.amount ?: 0.0 val betAmount = it.action.amount ?: 0.0
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0
val remainingStack = computedAction.stackBeforeActing val remainingStack = computedAction.stackBeforeActing
if (remainingStack != null && committedStack + remainingStack < betAmount) { if (remainingStack != null && remainingStack < betAmount) {
type = Action.Type.CALL_ALLIN type = Action.Type.CALL_ALLIN
} }
} ?: throw PAIllegalStateException("Can't call without a significant action") } ?: throw PAIllegalStateException("Can't call without a significant action")
@ -222,8 +219,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
} }
} }
Action.Type.BET, Action.Type.POT, Action.Type.RAISE -> { Action.Type.BET, Action.Type.POT, Action.Type.RAISE -> {
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0 if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
if (remainingStack != null && actionAmount != null && committedStack + remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL) setOf(Action.Type.FOLD, Action.Type.CALL)
} else { } else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN) setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
@ -232,7 +228,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
Action.Type.RAISE_ALLIN, Action.Type.BET_ALLIN -> { Action.Type.RAISE_ALLIN, Action.Type.BET_ALLIN -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) { if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL) setOf(Action.Type.FOLD, Action.Type.CALL)
} else if (activePositions(index).size == 1 && remainingStack != null && actionAmount != null && remainingStack > actionAmount) { } else if (activePositions(index).size == 2 && remainingStack != null && actionAmount != null && remainingStack > actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL) setOf(Action.Type.FOLD, Action.Type.CALL)
} else { } else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN) setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
@ -351,9 +347,8 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
val activePositions = activePositions(refIndex) val activePositions = activePositions(refIndex)
// Remove the reference position from acting, UNLESS it's the BB/Straddle and players have called // Remove the reference position from acting, UNLESS it's the BB and players have called
val preflop = referenceAction.action.type == Action.Type.POST_BB || referenceAction.action.type == Action.Type.STRADDLE if (!(referenceAction.action.type == Action.Type.POST_BB && getStreetNextCalls(refIndex).isNotEmpty())) {
if (!(preflop && getStreetNextCalls(refIndex).isNotEmpty())) {
activePositions.remove(refIndexPosition) activePositions.remove(refIndexPosition)
} }
@ -528,8 +523,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
created = true created = true
} }
val stack = val stack = this.filter { it.positionIndex == positionIndex }.sumByDouble { it.action.effectiveAmount }
this.filter { it.positionIndex == positionIndex }.sumOf { it.action.effectiveAmount }
playerSetup.stack = stack playerSetup.stack = stack
if (created) { if (created) {
@ -601,7 +595,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
*/ */
override fun getStreetNextCalls(index: Int): List<ComputedAction> { override fun getStreetNextCalls(index: Int): List<ComputedAction> {
val streetNextSignificantIndex = getStreetNextSignificantAction(index)?.action?.index val streetNextSignificantIndex = getStreetNextSignificantAction(index)?.action?.index
?: (this.lastIndexOfStreet(index) + 1) // +1 because of "until" ?: this.lastIndexOfStreet(index) + 1 // +1 because of "until"
return this.filter { return this.filter {
it.action.index in ((index + 1) until streetNextSignificantIndex) it.action.index in ((index + 1) until streetNextSignificantIndex)
&& (it.action.type?.isCall ?: false) && (it.action.type?.isCall ?: false)
@ -615,7 +609,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
} }
override fun totalPotSize(index: Int): Double { override fun totalPotSize(index: Int): Double {
return this.handHistory.anteSum + this.take(index).sumOf { it.action.effectiveAmount } return this.handHistory.anteSum + this.take(index).sumByDouble { it.action.effectiveAmount }
} }
/*** /***

@ -916,7 +916,7 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
this.handSetup.setStraddlePositions(this.firstStraddlePosition!!, positions) this.handSetup.setStraddlePositions(this.firstStraddlePosition!!, positions)
} }
this.handHistory.configure(this.handSetup, true) // restart initial setup this.handHistory.configure(this.handSetup) // restart initial setup
this.sortedActions.load(this.handHistory) // recreate the sorted Actions this.sortedActions.load(this.handHistory) // recreate the sorted Actions
this.createRowRepresentation() // make the table rows this.createRowRepresentation() // make the table rows
@ -1058,7 +1058,6 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
} }
fun toggleSettingsRows() { fun toggleSettingsRows() {
this.selectionLiveData.value = null
this.settingsExpanded = !this.settingsExpanded this.settingsExpanded = !this.settingsExpanded
this.createRowRepresentation() this.createRowRepresentation()
} }

@ -3,9 +3,9 @@ package net.pokeranalytics.android.ui.modules.handhistory.replayer
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
enum class FrameType(val visualOccurences: Int) { enum class FrameType(val visualOccurences: Int) {
STATE(50), STATE(150),
GATHER_ANIMATION(1), GATHER_ANIMATION(2),
DISTRIBUTION_ANIMATION(1) DISTRIBUTION_ANIMATION(2)
} }
class FrameManager { class FrameManager {

@ -3,13 +3,7 @@ package net.pokeranalytics.android.ui.modules.handhistory.replayer
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.ContentValues import android.content.ContentValues
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.MediaMuxer
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
@ -17,14 +11,13 @@ import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.arthenica.ffmpegkit.FFmpegKit
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.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.FFMPEG_DESCRIPTOR_FILE
import net.pokeranalytics.android.util.TriggerNotification import net.pokeranalytics.android.util.TriggerNotification
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
@ -32,7 +25,7 @@ import net.pokeranalytics.android.util.video.AnimatedGIFWriter
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Date import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
enum class FileType(var value: String) { enum class FileType(var value: String) {
@ -59,7 +52,11 @@ class ReplayExportService : Service() {
fun videoExport(handHistoryId: String) { fun videoExport(handHistoryId: String) {
this@ReplayExportService.handHistoryId = handHistoryId this@ReplayExportService.handHistoryId = handHistoryId
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startFFMPEGVideoExport() startFFMPEGVideoExport()
} else {
startFFMPEGVideoExportPreQ()
}
} }
fun gifExport(handHistoryId: String) { fun gifExport(handHistoryId: String) {
@ -149,8 +146,6 @@ class ReplayExportService : Service() {
private fun startFFMPEGVideoExport() { private fun startFFMPEGVideoExport() {
val start = Date().time
GlobalScope.launch(coroutineContext) { GlobalScope.launch(coroutineContext) {
val async = GlobalScope.async { val async = GlobalScope.async {
@ -162,6 +157,7 @@ class ReplayExportService : Service() {
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
@ -169,212 +165,120 @@ class ReplayExportService : Service() {
val drawer = TableDrawer() val drawer = TableDrawer()
drawer.configurePaints(context, animator) drawer.configurePaints(context, animator)
// generates all images and file descriptor
Timber.d("Generating images for video...")
val tmpDir = animator.generateVideoContent(this@ReplayExportService)
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE"
val formattedDate = Date().dateTimeFileFormatted val formattedDate = Date().dateTimeFileFormatted
val fileName = "hand_${formattedDate}.mp4" val fileName = "hand_${formattedDate}.mp4"
val outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) ?: throw PAIllegalStateException("File is invalid") val outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) ?: throw PAIllegalStateException("File is invalid")
val outputFile = File(outputDirectory, fileName) 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) {
when {
it.returnCode.isSuccess -> {
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))
}
}
Timber.d("Creating video with MediaMuxer...") File(dpath).delete()
tmpDir.delete()
try { val file = File(output)
createVideoWithMediaMuxer(animator, context, outputFile, width, height)
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) val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
Timber.d("getContentUri = $videoCollection...")
val fileDetails = ContentValues().apply { val fileDetails = ContentValues().apply {
Timber.d("set file details = $fileName") Timber.d("set file details = $fileName")
put(MediaStore.Video.Media.DISPLAY_NAME, fileName) put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
put(MediaStore.Video.Media.MIME_TYPE, FileType.VIDEO_MP4.value) put(MediaStore.Images.Media.MIME_TYPE, FileType.VIDEO_MP4.value)
} }
// copy video to nice path
resolver.insert(videoCollection, fileDetails)?.let { uri -> resolver.insert(videoCollection, fileDetails)?.let { uri ->
Timber.d("copy file at uri = $uri") Timber.d("copy file at uri = $uri")
val os = resolver.openOutputStream(uri) val os = resolver.openOutputStream(uri)
os?.write(outputFile.readBytes()) os?.write(file.readBytes())
os?.close() os?.close()
outputFile.delete() // delete temp file file.delete() // delete temp file
notifyUser(uri, FileType.VIDEO_MP4) notifyUser(uri, FileType.VIDEO_MP4)
val end = Date().time
Timber.d("video generation duration = ${end - start}")
} ?: run { } ?: run {
Timber.w("Resolver insert ended without uri...") Timber.w("Resolver insert ended without uri...")
} }
} catch (e: Exception) {
Timber.e(e, "Error creating video with MediaMuxer")
if (outputFile.exists()) {
outputFile.delete()
}
} }
realm.close()
} }
async.await() async.await()
} }
}
private fun createVideoWithMediaMuxer(animator: ReplayerAnimator, context: Context, outputFile: File, width: Int, height: Int) {
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val frameRate = 20
val bitRate = 2000000 // 2Mbps
// Create MediaFormat with YUV420 flexible format
val format = MediaFormat.createVideoFormat(mimeType, width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
}
// Create encoder
val encoder = MediaCodec.createEncoderByType(mimeType)
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
Timber.d("Starting encoder...")
encoder.start()
// Create MediaMuxer
val muxer = MediaMuxer(outputFile.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
var trackIndex = -1
var muxerStarted = false
val bufferInfo = MediaCodec.BufferInfo()
var frameIndex = 0
val presentationTimeUs = 1000000L / frameRate // Time per frame in microseconds
try {
// Generate frames using animator
Timber.d("Generate frames...")
animator.frames(context) { bitmap, visualOccurrences ->
// Timber.d(">>> Generated frame, visualOccurrences = $visualOccurrences")
val yuvData = convertBitmapToYUV420(bitmap, width, height)
repeat(visualOccurrences) {
// Convert bitmap to YUV420 and feed to encoder
val inputBufferIndex = encoder.dequeueInputBuffer(10000)
if (inputBufferIndex >= 0) {
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)
if (inputBuffer != null) {
inputBuffer.clear()
inputBuffer.put(yuvData)
encoder.queueInputBuffer(inputBufferIndex, 0, yuvData.size, frameIndex * presentationTimeUs, 0)
}
}
// Process output buffers
// Timber.d("drainEncoder...")
drainEncoder(encoder, muxer, bufferInfo, trackIndex) { newTrackIndex ->
trackIndex = newTrackIndex
muxerStarted = true
} }
frameIndex++ // private fun startVideoExport() {
} //
} // GlobalScope.launch(coroutineContext) {
Timber.d("end of frames generation...") // val c = GlobalScope.async {
//
// Signal end of input // val realm = Realm.getDefaultInstance()
val inputBufferIndex = encoder.dequeueInputBuffer(10000) // val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId")
if (inputBufferIndex >= 0) { //
encoder.queueInputBuffer(inputBufferIndex, 0, 0, frameIndex * presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM) // val context = this@ReplayExportService
} //
// val animator = ReplayerAnimator(handHistory, true)
Timber.d("drainEncoder again...") //
// val square = 1024
// Drain remaining output //
drainEncoder(encoder, muxer, bufferInfo, trackIndex, true) { newTrackIndex -> // val width = square
if (!muxerStarted) { // val height = square
trackIndex = newTrackIndex //
muxerStarted = true // animator.setDimension(width.toFloat(), height.toFloat())
} // TableDrawer.configurePaints(context, animator)
} //
// val muxer = MMediaMuxer()
} finally { // muxer.init(null, width, height, "hhVideo", "YES!")
Timber.d("stop and release...") //
// animator.frames(context) { bitmap, count ->
encoder.stop() //
encoder.release() // try {
if (muxerStarted) { // val byteArray = bitmap.toByteArray()
muxer.stop() // muxer.addFrame(byteArray, count, false)
} // } catch (e: Exception) {
muxer.release() // Timber.e("error = ${e.message}")
} // }
} //
// }
private fun drainEncoder(encoder: MediaCodec, muxer: MediaMuxer, bufferInfo: MediaCodec.BufferInfo, //
trackIndex: Int, endOfStream: Boolean = false, onTrackAdded: (Int) -> Unit) { // realm.close()
var localTrackIndex = trackIndex //
// muxer.createVideo { path ->
while (true) { // notifyUser(path)
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, if (endOfStream) 10000 else 0) // }
when { //
outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> { // }
if (!endOfStream) break else continue // c.await()
} // }
outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { //
if (localTrackIndex >= 0) { // }
throw RuntimeException("Format changed twice")
}
localTrackIndex = muxer.addTrack(encoder.outputFormat)
muxer.start()
onTrackAdded(localTrackIndex)
}
outputBufferIndex >= 0 -> {
val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)
if (outputBuffer != null && bufferInfo.size > 0 && localTrackIndex >= 0) {
muxer.writeSampleData(localTrackIndex, outputBuffer, bufferInfo)
}
encoder.releaseOutputBuffer(outputBufferIndex, false)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break
}
}
}
}
}
private fun convertBitmapToYUV420(bitmap: Bitmap, width: Int, height: Int): ByteArray {
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
val yuvSize = width * height * 3 / 2
val yuv = ByteArray(yuvSize)
var yIndex = 0
var uvIndex = width * height
for (y in 0 until height) {
for (x in 0 until width) {
val pixel = pixels[y * width + x]
val r = (pixel shr 16) and 0xff
val g = (pixel shr 8) and 0xff
val b = pixel and 0xff
// Convert RGB to YUV
val yValue = ((66 * r + 129 * g + 25 * b + 128) shr 8) + 16
yuv[yIndex++] = yValue.coerceIn(0, 255).toByte()
if (y % 2 == 0 && x % 2 == 0) {
val uValue = ((-38 * r - 74 * g + 112 * b + 128) shr 8) + 128
val vValue = ((112 * r - 94 * g - 18 * b + 128) shr 8) + 128
yuv[uvIndex++] = uValue.coerceIn(0, 255).toByte()
yuv[uvIndex++] = vValue.coerceIn(0, 255).toByte()
}
}
}
return yuv
}
private fun startGIFExportPreQ() { private fun startGIFExportPreQ() {
@ -438,6 +342,80 @@ class ReplayExportService : Service() {
} }
private fun startFFMPEGVideoExportPreQ() {
GlobalScope.launch(coroutineContext) {
val async = 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.configure(width.toFloat(), height.toFloat(), this@ReplayExportService)
val drawer = TableDrawer()
drawer.configurePaints(context, animator)
// generates all images and file descriptor
Timber.d("Generating images for video...")
val tmpDir = animator.generateVideoContent(this@ReplayExportService)
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE"
val formattedDate = Date().dateTimeFileFormatted
val output = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
"hand_${formattedDate}.mp4"
).path
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"
FFmpegKit.executeAsync(command) {
when {
it.returnCode.isSuccess -> {
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))
}
}
// 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 ->
//
// if (rc == RETURN_CODE_SUCCESS) {
// Timber.d("FFMPEG command execution completed successfully")
// } else if (rc == RETURN_CODE_CANCEL) {
// Timber.d("Command execution cancelled by user.")
// } else {
// Timber.d(String.format("Command execution failed with rc=%d and the output below.", rc))
// }
// Delete descriptor and image files
// tmpDir.delete()
// File(dpath).delete()
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)
@ -476,7 +454,7 @@ class ReplayExportService : Service() {
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 or PendingIntent.FLAG_IMMUTABLE) val pendingIntent = PendingIntent.getActivity(this, 0, chooser, PendingIntent.FLAG_CANCEL_CURRENT)
TriggerNotification(this, title, body, pendingIntent) TriggerNotification(this, title, body, pendingIntent)

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.RectF import android.graphics.RectF
import kotlinx.coroutines.android.awaitFrame
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Action import net.pokeranalytics.android.model.realm.handhistory.Action
@ -96,7 +97,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
* 2/ The last frame of the last step is longer because some video players * 2/ The last frame of the last step is longer because some video players
* auto-replay videos, and we want viewers to visualize the end. * auto-replay videos, and we want viewers to visualize the end.
*/ */
private val visualOccurrences: Int private val visualOccurences: Int
get() { get() {
val step = this.currentStep val step = this.currentStep
return when { return when {
@ -515,7 +516,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
* Generates images and image descriptor to build the video using ffmpeg * Generates images and image descriptor to build the video using ffmpeg
* Command line: https://trac.ffmpeg.org/wiki/Slideshow * Command line: https://trac.ffmpeg.org/wiki/Slideshow
*/ */
fun generateVideoContent(context: Context): File { suspend fun generateVideoContent(context: Context): File {
var ffmpegImageDescriptor = "" var ffmpegImageDescriptor = ""
var vo = 0.0 var vo = 0.0
@ -535,7 +536,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
val bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
vo = this.visualOccurrences / 90.0 // this is needed before the call to drawTable which pass to the next frame vo = this.visualOccurences / 90.0 // this is needed before the call to drawTable which pass to the next frame
this.drawer.drawTable(canvas, context) this.drawer.drawTable(canvas, context)
@ -549,7 +550,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
count++ count++
// awaitFrame() awaitFrame()
} }
nextStep() nextStep()
} }
@ -587,12 +588,11 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
// Timber.d("FRAME [$f] >> step: $currentStepIndex, frame: $currentFrameIndex, vo: ${this.visualOccurences}, type: ${this.frameType}") // Timber.d("FRAME [$f] >> step: $currentStepIndex, frame: $currentFrameIndex, vo: ${this.visualOccurences}, type: ${this.frameType}")
val bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
val vo = this.visualOccurrences // this is needed before the call to drawTable which pass to the next frame val vo = this.visualOccurences // this is needed before the call to drawTable which pass to the next frame
this.drawer.drawTable(canvas, context) this.drawer.drawTable(canvas, context)
frameHandler(bitmap, vo) frameHandler(bitmap, vo)
bitmap.recycle()
} }

@ -32,9 +32,13 @@ class ReplayerView(context: Context, attrs: AttributeSet) : View(context, attrs)
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas) super.onDraw(canvas)
canvas?.let {
this.animator.drawTable(canvas, context) this.animator.drawTable(canvas, context)
} }
} }
}

@ -1,10 +1,7 @@
package net.pokeranalytics.android.ui.modules.handhistory.replayer package net.pokeranalytics.android.ui.modules.handhistory.replayer
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.*
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.RectF
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -253,7 +250,7 @@ class TableDrawer {
cardRects.forEachIndexed { j, cardRect -> cardRects.forEachIndexed { j, cardRect ->
val showVillainHands = Preferences.getShowVillainCards(context) val showVillainHands = Preferences.getShowVillainCards(context)
if (j < (cards?.size ?: 0) && (showVillainHands || isHero)) { // show card if (j < cards?.size ?: 0 && (showVillainHands || isHero)) { // show card
val card = cards?.get(j)!! // tested line before val card = cards?.get(j)!! // tested line before
drawCard(card, cardRect, canvas, context) drawCard(card, cardRect, canvas, context)
} else { // show hidden cards } else { // show hidden cards

@ -19,6 +19,7 @@ import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.view.GridSpacingItemDecoration import net.pokeranalytics.android.ui.view.GridSpacingItemDecoration
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.noGroupingFormatted
import timber.log.Timber import timber.log.Timber
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
@ -151,12 +152,12 @@ class KeyboardAmountView : AbstractKeyboardView,
fun setEditText(editText: EditText, amount: Double?) { fun setEditText(editText: EditText, amount: Double?) {
// Timber.d("edit text = $editText") // Timber.d("edit text = $editText")
editText.isEnabled = true // avoid crashes due to input connection null
this.setInputConnection(editText) this.setInputConnection(editText)
// editText.setText(amount?.noGroupingFormatted) editText.setText(amount?.noGroupingFormatted)
// editText.requestFocus() editText.requestFocus()
editText.isEnabled = true // avoid crashes due to input connection null
} }

@ -68,11 +68,6 @@ class SessionActivity: BaseActivity() {
initUI() initUI()
} }
// override fun onPause() {
// super.onPause()
// this.paApplication.backupOperator?.backupIfNecessary()
// }
override fun onBackPressed() { override fun onBackPressed() {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
super.onBackPressed() super.onBackPressed()

@ -11,8 +11,6 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.interpolator.view.animation.FastOutSlowInInterpolator 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 androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -37,10 +35,7 @@ import net.pokeranalytics.android.ui.helpers.DateTimePickerManager
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.handhistory.HandHistoryActivity import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.RowRepresentableDiffCallback
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
@ -80,7 +75,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// CrashLogging.log("session (id=${this.currentSession.id}): valid=${currentSession.isValid}, managed=${currentSession.isManaged}, loaded=${currentSession.isLoaded} ") CrashLogging.log("session (id=${this.currentSession.id}): valid=${currentSession.isValid}, managed=${currentSession.isManaged}, loaded=${currentSession.isLoaded} ")
this.refreshTimer() this.refreshTimer()
} }
@ -130,10 +125,10 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
// val viewManager = SmoothScrollLinearLayoutManager(requireContext()) val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.recyclerView.apply { binding.recyclerView.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = LinearLayoutManager(requireContext()) layoutManager = viewManager
} }
binding.floatingActionButton.setOnClickListener { binding.floatingActionButton.setOnClickListener {
@ -410,7 +405,7 @@ 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 { GlobalScope.launch(coroutineContext) {
var optimalDuration: Double? = null var optimalDuration: Double? = null

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save