Compare commits

..

1 Commits

  1. 139
      CLAUDE.md
  2. 92
      app/build.gradle
  3. 22
      app/proguard-rules.pro
  4. 19
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/FavoriteSessionUnitTest.kt
  5. 8
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  6. 70
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt
  7. 14
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  8. 3
      app/src/debug/AndroidManifest.xml
  9. 143
      app/src/main/AndroidManifest.xml
  10. BIN
      app/src/main/ic_launcher-playstore.png
  11. 39
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  12. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  13. 49
      app/src/main/java/net/pokeranalytics/android/api/BlogPostApi.kt
  14. 60
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  15. 47
      app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt
  16. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  17. 2
      app/src/main/java/net/pokeranalytics/android/calcul/AggregationTypeExtensions.kt
  18. 2
      app/src/main/java/net/pokeranalytics/android/calcul/ComputedResultsExtensions.kt
  19. 2
      app/src/main/java/net/pokeranalytics/android/calcul/ReportDisplay.kt
  20. 2
      app/src/main/java/net/pokeranalytics/android/calcul/ReportExtensions.kt
  21. 8
      app/src/main/java/net/pokeranalytics/android/calcul/StatRepresentable.kt
  22. 73
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  23. 29
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  24. 18
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  25. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  26. 42
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  27. 28
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  28. 2
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  29. 297
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  30. 9
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  31. 46
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  32. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  33. 13
      app/src/main/java/net/pokeranalytics/android/model/Stakes.kt
  34. 13
      app/src/main/java/net/pokeranalytics/android/model/blogpost/BlogPost.kt
  35. 9
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  36. 35
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  37. 238
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  38. 56
      app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt
  39. 189
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  40. 168
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  41. 137
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  42. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  43. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  44. 7
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  45. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  46. 131
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  47. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  48. 18
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  49. 207
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  50. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  51. 66
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  52. 11
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  53. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  54. 56
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt
  55. 245
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  56. 15
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  57. 9
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  58. 20
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  59. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  60. 160
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  61. 18
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  62. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt
  63. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt
  64. 42
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  65. 187
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt
  66. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt
  67. 128
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  68. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt
  69. 42
      app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt
  70. 5
      app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt
  71. 296
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  72. 49
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  73. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  74. 53
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  75. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt
  76. 197
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  77. 198
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  78. 121
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  79. 121
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  80. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/Top10Fragment.kt
  81. 44
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  82. 21
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt
  83. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt
  84. 30
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  85. 4
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetDoubleEditTextFragment.kt
  86. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextFragment.kt
  87. 9
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetEditTextMultiLinesFragment.kt
  88. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  89. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListFragment.kt
  90. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt
  91. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetNumericTextFragment.kt
  92. 151
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStakesFragment.kt
  93. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt
  94. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetSumFragment.kt
  95. 4
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetTableSizeGridFragment.kt
  96. 5
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetType.kt
  97. 41
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt
  98. 4
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComparisonReportFragment.kt
  99. 49
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  100. 36
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.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 29
buildToolsVersion "30.0.3" buildToolsVersion "29.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 29
versionCode 180 versionCode 121
versionName "6.0.38" versionName "5.1.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -68,15 +69,25 @@ android {
} }
} }
flavorDimensions 'endOfUse' flavorDimensions 'endOfUse'
productFlavors { // already used: 50000, 51000, 52000, 52130, 52110, 52120 productFlavors { // already used: 50000, 51000, 52000
standard { standard {
dimension = 'endOfUse' dimension = 'endOfUse'
} }
// oct2021 { april2021 {
// dimension = 'endOfUse' dimension = 'endOfUse'
// versionNameSuffix = '_oct2021' versionNameSuffix = '_april2021'
// versionCode = 52120 + android.defaultConfig.versionCode versionCode = 52130 + android.defaultConfig.versionCode
// } }
nov2020 {
dimension = 'endOfUse'
versionNameSuffix = '_nov2020'
versionCode = 52110 + android.defaultConfig.versionCode
}
oct2021 {
dimension = 'endOfUse'
versionNameSuffix = '_oct2021'
versionCode = 52120 + android.defaultConfig.versionCode
}
} }
configurations { configurations {
@ -88,10 +99,6 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
@ -99,29 +106,26 @@ 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'
implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.core:core-ktx:1.3.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.2.1'
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.4.0'
// 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 +147,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:mobile-ffmpeg-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(...);
}

@ -30,9 +30,12 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
s2.endDate = Date() s2.endDate = Date()
s3.endDate = Date() s3.endDate = Date()
s1.cgBlinds = "2/4" s1.cgBigBlind = 4.0
s2.cgBlinds = "2/4" s1.cgSmallBlind = 2.0
s3.cgBlinds = "1/2" s2.cgBigBlind = 4.0
s2.cgSmallBlind = 2.0
s3.cgBigBlind = 1.0
s3.cgSmallBlind = 1.0
realm.insert(s1) realm.insert(s1)
realm.insert(s2) realm.insert(s2)
@ -42,7 +45,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext) val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) { if (favSession != null) {
Assert.assertEquals(4.0, favSession.cgBiggestBet) Assert.assertEquals(4.0, favSession.cgBigBlind)
} else { } else {
Assert.fail("session shouldn't be null") Assert.fail("session shouldn't be null")
} }
@ -62,9 +65,9 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val loc1 = realm.createObject(Location::class.java, "1") val loc1 = realm.createObject(Location::class.java, "1")
val loc2 = realm.createObject(Location::class.java, "2") val loc2 = realm.createObject(Location::class.java, "2")
s1.cgBiggestBet = 4.0 s1.cgBigBlind = 4.0
s2.cgBiggestBet = 4.0 s2.cgBigBlind = 4.0
s3.cgBiggestBet = 1.0 s3.cgBigBlind = 1.0
s1.location = loc1 s1.location = loc1
s2.location = loc1 s2.location = loc1
@ -78,7 +81,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext) val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) { if (favSession != null) {
Assert.assertEquals(1.0, favSession.cgBiggestBet) Assert.assertEquals(1.0, favSession.cgBigBlind)
} else { } else {
Assert.fail("session shouldn't be null") Assert.fail("session shouldn't be null")
} }

@ -44,8 +44,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s2.result?.buyin = 200.0 s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300 s2.result?.cashout = 500.0 // net result = 300
s1.cgBlinds = "0.5" // bb net result = -200bb s1.cgBigBlind = 0.5 // bb net result = -200bb
s2.cgBlinds = "2.0" // bb net result = 150bb s2.cgBigBlind = 2.0 // bb net result = 150bb
s2.tableSize = 5 s2.tableSize = 5
@ -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")

@ -28,25 +28,28 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1" s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1) val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake() val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyStake().apply { val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!) listOfValues = arrayListOf(s1.blinds!!)
} }
// blind.filterSectionRow = FilterSectionRow.Blind // blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes) val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Blind)
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -71,27 +74,30 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1" s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1) val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake() val filter = QueryCondition.AnyBlind()
val blind1 = QueryCondition.AnyStake().apply { val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!) listOfValues = arrayListOf(s1.blinds!!)
} }
val blind2 = QueryCondition.AnyStake().apply { val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!) listOfValues = arrayListOf(s2.blinds!!)
} }
val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Stakes) val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Blind)
filter.updateValueBy(filterElements) filter.updateValueBy(filterElements)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -116,24 +122,28 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1"
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b2) val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake() val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyStake().apply { val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s3.blinds!!) listOfValues = arrayListOf(s3.blinds!!)
} }
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes) val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Blind)
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -157,27 +167,31 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1" s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 2.0
s2.cgSmallBlind = 1.0
val s3 = Session.testInstance(100.0, false, Date(), 1, b2) val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val stake1 = QueryCondition.AnyStake().apply { val filter = QueryCondition.AnyBlind()
listOfValues = arrayListOf(s1.cgStakes!!)
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
} }
val stake2 = QueryCondition.AnyStake().apply { val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.cgStakes!!) listOfValues = arrayListOf(s2.blinds!!)
} }
val filterElement = FilterCondition(arrayListOf(stake1, stake2), FilterSectionRow.Stakes) val filterElement = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Blind)
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -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))
@ -536,11 +536,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s3 = Session.testInstance(netResult = 500.0) val s3 = Session.testInstance(netResult = 500.0)
s3.cgBlinds = "2.0" s3.cgBigBlind = 2.0
s3.result!!.buyin = 1000.0 s3.result!!.buyin = 1000.0
val s4 = Session.testInstance(netResult = 570.0) val s4 = Session.testInstance(netResult = 570.0)
s4.cgBlinds = "5.0" s4.cgBigBlind = 5.0
s4.result!!.buyin = 200.0 s4.result!!.buyin = 200.0
realm.commitTransaction() realm.commitTransaction()
@ -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,21 +1,31 @@
<?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:requestLegacyExternalStorage="true"
has been added due to a crash:
Fatal Exception: java.io.FileNotFoundException
/storage/emulated/0/Movies/gif_20_09_18_09_59_41.gif: open failed: EACCES (Permission denied)
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> happening here:
<uses-feature android:name="android.hardware.camera.any" android:required="false" /> net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService$startGIFExport$1$c$1.invokeSuspend (ReplayExportService.java:88)
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
The temporary fix comes from here:
https://medium.com/@sriramaripirala/android-10-open-failed-eacces-permission-denied-da8b630a89df
-->
<application <application
android:name=".PokerAnalyticsApplication" android:name=".PokerAnalyticsApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -32,8 +42,7 @@
android:name="net.pokeranalytics.android.ui.activity.HomeActivity" android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -46,8 +55,7 @@
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity" android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait">
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError"> <intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -62,186 +70,121 @@
</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" /> android:windowSoftInputMode="adjustNothing" />
<!-- 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="singleTop"
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
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop"
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,8 +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(9)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
.build() .build()
@ -67,33 +62,19 @@ class PokerAnalyticsApplication : Application() {
// Logs // Logs
Timber.plant(PokerAnalyticsLogs()) Timber.plant(PokerAnalyticsLogs())
} }
Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
Timber.d("Realm path = ${Realm.getDefaultInstance().path}") Timber.d("Realm path = ${Realm.getDefaultInstance().path}")
// 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 +87,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,49 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import com.android.volley.Request
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.Volley
import org.json.JSONArray
import timber.log.Timber
data class BlogPost(var id: Int, var content: String)
private fun JSONArray.toBlogPosts(): List<BlogPost> {
val posts = mutableListOf<BlogPost>()
(0 until this.length()).forEach { index ->
val jo = this.getJSONObject(index)
val post = BlogPost(jo.getInt("id"), jo.getJSONObject("content").getString("rendered"))
posts.add(post)
}
return posts
}
class BlogPostApi {
companion object {
private const val tipsLastPostsURL = "https://www.poker-analytics.net/blog/wp-json/wp/v2/posts/?categories=109\n"
fun getLatestPosts(context: Context, callback: (List<BlogPost>) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val jsonObjectRequest = JsonArrayRequest(
Request.Method.GET, tipsLastPostsURL, null,
{ response ->
// Timber.d("posts = $response")
callback(response.toBlogPosts())
},
{ error ->
Timber.w("Error while retrieving blog posts: $error")
}
)
queue.add(jsonObjectRequest)
}
}
}

@ -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) {
@ -201,6 +202,7 @@ class Calculator {
val report = Report(options) val report = Report(options)
groups.forEach { group -> groups.forEach { group ->
// val s = Date()
// Clean existing computables / sessionSets if group is reused // Clean existing computables / sessionSets if group is reused
group.cleanup() group.cleanup()
@ -240,27 +242,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)
@ -276,52 +265,39 @@ class Calculator {
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble() val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(TOTAL_BUYIN, totalBuyin) results.addStat(TOTAL_BUYIN, totalBuyin)
val totalTips = computables.sum(ComputableResult.Field.RATED_TIPS.identifier).toDouble()
results.addStat(TOTAL_TIPS, totalTips)
// Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}") // Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}")
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)
} }
// val shouldComputeITMRatio = options.stats.contains(TOURNAMENT_ITM_RATIO) || computableGroup.displayedStats?.contains(TOURNAMENT_ITM_RATIO) == true
// if (shouldComputeITMRatio) {
// val itmCount = computables.count { it.session?.result?.cashout ?: 0.0 > 0.0 } // should we add a property inside ComputableResult for better performance?
// val itmRatio = itmCount.toDouble() / computables.size.toDouble()
// results.addStat(TOURNAMENT_ITM_RATIO, itmRatio)
// }
if (options.computeLocationsPlayed) { if (options.computeLocationsPlayed) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble()) results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
} }
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 avgBuyin = totalBuyin / computables.size.toDouble() val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats( results.addStats(
setOf( setOf(
ComputedStat(AVERAGE, average), ComputedStat(AVERAGE, average),
ComputedStat(WIN_RATIO, winRatio), ComputedStat(WIN_RATIO, winRatio),
ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio),
ComputedStat(AVERAGE_BUYIN, avgBuyin) ComputedStat(AVERAGE_BUYIN, avgBuyin)
) )
) )
@ -348,7 +324,6 @@ class Calculator {
var longestWinStreak = 0 var longestWinStreak = 0
var longestLoseStreak = 0 var longestLoseStreak = 0
var currentStreak = 0 var currentStreak = 0
var tITMCount = 0
computables.forEach { computable -> computables.forEach { computable ->
index++ index++
@ -356,7 +331,6 @@ class Calculator {
tBBSum += computable.bbNet tBBSum += computable.bbNet
tBBSessionCount += computable.hasBigBlind tBBSessionCount += computable.hasBigBlind
tWinningSessionCount += computable.isPositive tWinningSessionCount += computable.isPositive
tITMCount += computable.isPositive
tBuyinSum += computable.ratedBuyin tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands tHands += computable.estimatedHands
@ -382,16 +356,11 @@ 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,
data = session data = session
) )
results.addEvolutionValue(
tITMCount.toDouble() / index.toDouble(),
stat = TOURNAMENT_ITM_RATIO,
data = session)
results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session) results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session)
results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session) results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session)
@ -433,9 +402,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 +502,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 +588,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 stats: List<Stat>? = null) {
/** /**
* A subgroup used to compute stat variation * A subgroup used to compute stat variation
@ -31,11 +32,6 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
return this.query.conditions return this.query.conditions
} }
/**
* The size of the retrieved computables list
*/
var size: Int = 0
/** /**
* The list of endedSessions to compute * The list of endedSessions to compute
*/ */
@ -55,10 +51,7 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
val sortedField = if (sorted) "session.startDate" else null val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField) val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this.size = computables.size
this._computables = computables this._computables = computables
return computables return computables
} }
@ -84,20 +77,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
*/
fun cleanup() { fun cleanup() {
this._computables = null this._computables = null
this._sessionSets = null this._sessionSets = null

@ -3,6 +3,7 @@ package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues import net.pokeranalytics.android.ui.view.DefaultLegendValues
@ -31,23 +32,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),
@ -54,8 +53,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
TOTAL_BUYIN(27), TOTAL_BUYIN(27),
RISK_OF_RUIN(28), RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29), STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31)
; ;
companion object : IntSearchable<Stat> { companion object : IntSearchable<Stat> {
@ -103,8 +100,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,13 +124,11 @@ 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
TOURNAMENT_ITM_RATIO -> R.string.itm_ratio
TOTAL_TIPS -> R.string.total_tips
else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined") else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined")
} }
} }
@ -152,7 +145,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)
} }
@ -168,7 +161,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> { HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
return TextFormat(value.formattedHourlyDuration()) return TextFormat(value.formattedHourlyDuration())
} // red/green percentages } // red/green percentages
WIN_RATIO, ROI, TOURNAMENT_ITM_RATIO -> { WIN_RATIO, ROI -> {
val color = if (value * 100 >= this.threshold) R.color.green else R.color.red val color = if (value * 100 >= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color) return TextFormat("${(value * 100).formatted}%", color)
} }
@ -176,9 +169,9 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val color = if (value * 100 <= this.threshold) R.color.green else R.color.red val color = if (value * 100 <= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color) return TextFormat("${(value * 100).formatted}%", color)
} }
// white amounts // white amountsr
AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN, TOTAL_TIPS -> { STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN -> {
return TextFormat(value.toCurrency(currency)) return TextFormat(value.toCurrency(currency))
} }
LONGEST_STREAKS -> { LONGEST_STREAKS -> {
@ -192,7 +185,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() { get() {
return when (this) { return when (this) {
RISK_OF_RUIN -> 5.0 RISK_OF_RUIN -> 5.0
TOURNAMENT_ITM_RATIO -> 10.0
WIN_RATIO -> 50.0 WIN_RATIO -> 50.0
else -> 0.0 else -> 0.0
} }
@ -208,12 +200,12 @@ 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
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
WIN_RATIO, HOURLY_DURATION, TOURNAMENT_ITM_RATIO -> return this.localizedTitle(context) WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context)
else -> null else -> null
} }
resId?.let { resId?.let {
@ -247,8 +239,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() { get() {
return when (this) { return when (this) {
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB, HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, HANDS_PLAYED, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_TIPS -> false STANDARD_DEVIATION_BB_PER_100_HANDS -> false
else -> true else -> true
} }
} }
@ -265,7 +257,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val legendHideRightValue: Boolean val legendHideRightValue: Boolean
get() { get() {
return when (this) { return when (this) {
AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, WIN_RATIO, TOURNAMENT_ITM_RATIO, AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, WIN_RATIO,
HOURLY_DURATION, AVERAGE_HOURLY_DURATION -> true HOURLY_DURATION, AVERAGE_HOURLY_DURATION -> true
else -> false else -> false
} }
@ -277,8 +269,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val graphSignificantIndividualValue: Boolean val graphSignificantIndividualValue: Boolean
get() { get() {
return when (this) { return when (this) {
AVERAGE, WIN_RATIO, TOURNAMENT_ITM_RATIO, AVERAGE, WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES,
NUMBER_OF_SETS, NUMBER_OF_GAMES,
STANDARD_DEVIATION, HOURLY_DURATION -> false STANDARD_DEVIATION, HOURLY_DURATION -> false
else -> true else -> true
} }
@ -324,8 +315,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return when (this) { return when (this) {
NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB, NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
AVERAGE_HOURLY_DURATION, HOURLY_DURATION, AVERAGE_HOURLY_DURATION, HOURLY_DURATION,
NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, WIN_RATIO,
WIN_RATIO, TOURNAMENT_ITM_RATIO,
AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true
else -> false else -> false
} }

@ -39,28 +39,10 @@ class BankrollCalculator {
initialValue += bankroll.initialValue * rate initialValue += bankroll.initialValue * rate
} }
if (setup.virtualBankroll) { bankroll.transactions?.let { transactions ->
val sum = realm.where(Transaction::class.java) val sum = transactions.sum("amount")
.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() transactionNet += rate * sum.toDouble()
} else {
bankroll.transactions?.let { transactions ->
val sum = transactions.sum("amount")
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")
} }
/** /**

@ -21,163 +21,150 @@ import kotlin.math.round
*/ */
class CashGameOptimalDurationCalculator { 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 polynomialDegree = 7 // the degree of the computed polynomial
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 * Starts the calculation
* [isLive] is a boolean to indicate if we're looking at live or online games
/*** * return a duration or null if it could not be computed
* Starts the calculation */
* [isLive] is a boolean to indicate if we're looking at live or online games fun start(isLive: Boolean): Double? {
* return a duration or null if it could not be computed
*/ val realm = Realm.getDefaultInstance()
fun start(isLive: Boolean): Double? {
val query = Query().add(QueryCondition.IsCash) // cash game
val realm = Realm.getDefaultInstance() query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online
query.add(QueryCondition.EndDateNotNull) // ended
val query = Query().add(QueryCondition.IsCash) // cash game query.add(QueryCondition.BigBlindNotNull) // has BB value
query.add(
if (isLive) { val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
QueryCondition.IsLive val sessionsByDuration = sessions.groupBy {
} else { val dur = round((it.netDuration / bucket).toDouble()) * bucket
QueryCondition.IsOnline
}
) // live / online
query.add(QueryCondition.EndDateNotNull) // ended
query.add(QueryCondition.BiggestBetNotNull) // has BB value
val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
val sessionsByDuration = sessions.groupBy {
val dur = round((it.netDuration / bucket).toDouble()) * bucket
// Timber.d("Stop notif > key: $dur") // Timber.d("Stop notif > key: $dur")
dur dur
} }
// define validity interval // define validity interval
var start: Double? = null var start: Double? = null
var end: Double? = null var end: Double? = null
var validBuckets = 0 var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
// Timber.d("Stop notif > keys: $hkeys ") Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) { for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0 val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) { if (start == null && sessionCount >= minimumValidityCount) {
start = key start = key
} }
if (sessionCount >= minimumValidityCount) { if (sessionCount >= minimumValidityCount) {
end = key end = key
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)
var bestAverage = 0.0
var bestAverage = 0.0 var bestHourlyRate = 0.0
var bestHourlyRate = 0.0 var bestDuration = 0.0
var bestDuration = 0.0 var maxDuration = 0.0
var maxDuration = 0.0
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
val duration = key + i * bucket / bucketInterval
val averageResult = getBB(duration, p)
val averageResult = getBB(duration, p) val hourly = averageResult / duration
val hourly = averageResult / duration if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) {
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { bestAverage = averageResult
bestAverage = averageResult bestDuration = duration
bestDuration = duration }
}
if (duration > 0 && hourly > bestHourlyRate) {
if (duration > 0 && hourly > bestHourlyRate) { bestHourlyRate = hourly
bestHourlyRate = hourly }
} if (duration > maxDuration){
if (duration > maxDuration) { maxDuration = duration
maxDuration = duration }
} }
}
}
}
if (bestDuration > 0.0) {
if (bestDuration > 0.0) { 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 }
}
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double { var y = 0.0
var y = 0.0 for (i in polynomial.indices) {
for (i in polynomial.indices) { y += polynomial[i] * netDuration.pow(i)
y += polynomial[i] * netDuration.pow(i) }
} return y
return y }
}
private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray {
private fun polynomialRegression(
sessions: List<Session>, val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
bbStandardDeviation: Double?
): DoubleArray { val points = WeightedObservedPoints()
val now = Date().time
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
sessions.forEach {
val points = WeightedObservedPoints() var weight = 5.0
val now = Date().time
val endTime = it.endDate?.time ?: 0L
sessions.forEach {
var weight = 5.0 val age = now - endTime
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point
val endTime = it.endDate?.time ?: 0L weight -= 1.0
}
val age = now - endTime if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point weight -= 3.0
weight -= 1.0 }
}
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points points.add(weight, it.netDuration.toDouble(), it.bbNet)
weight -= 3.0
} }
points.add(weight, it.netDuration.toDouble(), it.bbNet)
}
// polynomial of 7 degree, same as iOS // polynomial of 7 degree, same as iOS
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList()) return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
} }
} }
} }

@ -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")

@ -7,10 +7,10 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison
import net.pokeranalytics.android.model.Criteria.Blinds.comparison
import net.pokeranalytics.android.model.Criteria.Games.comparison import net.pokeranalytics.android.model.Criteria.Games.comparison
import net.pokeranalytics.android.model.Criteria.Limits.comparison import net.pokeranalytics.android.model.Criteria.Limits.comparison
import net.pokeranalytics.android.model.Criteria.Locations.comparison import net.pokeranalytics.android.model.Criteria.Locations.comparison
import net.pokeranalytics.android.model.Criteria.Stakes.comparison
import net.pokeranalytics.android.model.Criteria.TableSizes.comparison import net.pokeranalytics.android.model.Criteria.TableSizes.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison
@ -68,7 +68,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach { realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
objects.add(QueryCondition.CustomFieldListQuery(it)) objects.add(QueryCondition.CustomFieldListQuery(it))
} }
objects.sort() objects.sorted()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -104,10 +104,11 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
} }
objects.add(condition) objects.add(condition)
} }
objects.sort() objects.sorted()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
QueryCondition.distinct<Session, T, S>()?.let { QueryCondition.distinct<Session, T, S>()?.let {
val values = it.mapNotNull { session -> val values = it.mapNotNull { session ->
when (this) { when (this) {
@ -123,8 +124,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is TournamentFees -> if (session.tournamentEntryFee is S) { is TournamentFees -> if (session.tournamentEntryFee is S) {
session.tournamentEntryFee as S session.tournamentEntryFee as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is Stakes -> if (session.cgStakes is S) { is Blinds -> if (session.blinds is S) {
session.cgStakes as S session.blinds as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
else -> null else -> null
} }
@ -158,13 +159,12 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14) object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14)
object Years : ListCriteria(15) object Years : ListCriteria(15)
object AllMonthsUpToNow : ListCriteria(16) object AllMonthsUpToNow : ListCriteria(16)
object Stakes : ListCriteria(17) object Blinds : ListCriteria(17)
object TournamentFees : ListCriteria(18) object TournamentFees : ListCriteria(18)
object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19) object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
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() {
@ -237,34 +237,19 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.close() realm.close()
years years
} }
is Stakes -> comparison<QueryCondition.AnyStake, String>() is Blinds -> comparison<QueryCondition.AnyBlind, String>()
is ListCustomFields -> comparison<CustomFieldEntry>() is ListCustomFields -> comparison<CustomFieldEntry>()
is ValueCustomFields -> { is ValueCustomFields -> {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
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)
} }
} }
@ -286,7 +271,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
DayPeriods -> R.string.weekdays_or_weekend DayPeriods -> R.string.weekdays_or_weekend
Years -> R.string.year Years -> R.string.year
AllMonthsUpToNow -> R.string.month AllMonthsUpToNow -> R.string.month
Stakes -> R.string.blind Blinds -> R.string.blind
TournamentFees -> R.string.entry_fees TournamentFees -> R.string.entry_fees
// is ListCustomFields -> this.customField.resId // is ListCustomFields -> this.customField.resId
// is ValueCustomFields -> this.customField.resId // is ValueCustomFields -> this.customField.resId
@ -299,13 +284,13 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> { inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> {
val objects = mutableListOf<S>() val objects = mutableListOf<S>()
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.where<T>().sort("name").findAll().forEach { realm.where<T>().findAll().forEach {
val condition = (QueryCondition.getInstance<T>() as S).apply { val condition = (QueryCondition.getInstance<T>() as S).apply {
setObject(it) setObject(it)
} }
objects.add(condition) objects.add(condition)
} }
// objects.sort() objects.sorted()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -318,7 +303,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
} }
objects.add(condition) objects.add(condition)
} }
objects.sort() objects.sorted()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -334,8 +319,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
TournamentFeatures, Limits, TableSizes, TournamentTypes, TournamentFeatures, Limits, TableSizes, TournamentTypes,
MonthsOfYear, DaysOfWeek, SessionTypes, MonthsOfYear, DaysOfWeek, SessionTypes,
BankrollTypes, DayPeriods, Years, BankrollTypes, DayPeriods, Years,
AllMonthsUpToNow, AllMonthsUpToNow, Blinds, TournamentFees
Stakes, TournamentFees
) )
} }
} }

@ -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)
}
}

@ -1,13 +0,0 @@
package net.pokeranalytics.android.model
data class Stakes(var blinds: String?, var ante: Double?) {
companion object {
}
}

@ -1,13 +0,0 @@
package net.pokeranalytics.android.model.blogpost
import com.google.gson.annotations.SerializedName
class BlogPost {
@SerializedName("id")
var id: Int = 0
@SerializedName("level")
var content: String = ""
}

@ -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
@ -76,8 +75,8 @@ fun Session.getFormattedGameType(context: Context): String {
parameters.add(context.getString(R.string.tournament).capitalize()) parameters.add(context.getString(R.string.tournament).capitalize())
} }
} else { } else {
if (this.cgAnte != null || this.cgBlinds != null) { if (cgSmallBlind != null && cgBigBlind != null) {
parameters.add(getFormattedStakes()) parameters.add(getFormattedBlinds())
} }
game?.let { game?.let {
parameters.add(getFormattedGame()) parameters.add(getFormattedGame())
@ -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,13 +11,9 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query { class Query {
constructor(query: Query) { constructor(vararg elements: QueryCondition) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
this.add(elements.filterNotNull()) this.add(elements.asList())
} }
} }
@ -39,7 +35,7 @@ class Query {
return this return this
} }
fun add(queryConditions: List<QueryCondition>): Query { fun add(queryConditions: List<QueryCondition>): Query{
this._conditions.addAll(queryConditions) this._conditions.addAll(queryConditions)
return this return this
} }
@ -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.getDisplayName(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
}
} }

@ -12,10 +12,8 @@ import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.interfaces.CodedStake
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.StakesHolder
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
@ -23,7 +21,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 +53,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 +78,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 +102,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 +114,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 +122,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 -> {}
} }
@ -154,17 +140,6 @@ sealed class QueryCondition : RowRepresentable {
abstract var operator: Operator abstract var operator: Operator
override fun getDisplayName(context: Context): String {
this.resId?.let {
return context.getString(it)
}
return baseId
}
open fun getDisplayNameWithValues(context: Context): String {
return getDisplayName(context)
}
abstract class ListOfValues<T> : QueryCondition(), Comparable<ListOfValues<T>> where T : Comparable<T> { abstract class ListOfValues<T> : QueryCondition(), Comparable<ListOfValues<T>> where T : Comparable<T> {
abstract var listOfValues: MutableList<T> abstract var listOfValues: MutableList<T>
@ -174,14 +149,6 @@ sealed class QueryCondition : RowRepresentable {
return getDisplayName(context) return getDisplayName(context)
} }
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.listOfValues)
}
override fun getDisplayName(context: Context): String {
return getDisplayName(context, this.listOfValues)
}
private fun getDisplayName(context: Context, values: List<T>): String { private fun getDisplayName(context: Context, values: List<T>): String {
val prefix = this.resId?.let { val prefix = this.resId?.let {
context.getString(it) + " " context.getString(it) + " "
@ -194,6 +161,10 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
override fun getDisplayName(context: Context): String {
return getDisplayName(context, this.listOfValues)
}
override fun compareTo(other: ListOfValues<T>): Int { override fun compareTo(other: ListOfValues<T>): Int {
return listOfValues.sorted().first().compareTo(other.listOfValues.sorted().first()) return listOfValues.sorted().first().compareTo(other.listOfValues.sorted().first())
} }
@ -205,37 +176,28 @@ sealed class QueryCondition : RowRepresentable {
} }
abstract class SingleValue<T>(value: T) : QueryCondition() where T : Comparable<T> { abstract class SingleValue<T>(value: T) : QueryCondition() where T : Comparable<T> {
// override var listOfValues = mutableListOf<T>()
var singleValue: T = value var singleValue: T = value
abstract fun labelForValue(value: T, context: Context): String abstract fun labelForValue(value: T, context: Context): String
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.resId?.let { return getDisplayName(context, singleValue)
context.getString(it)
} ?: ""
// return getDisplayName(context, this.singleValue)
} }
override fun getDisplayNameWithValues(context: Context): String { fun getDisplayName(context: Context, value: T): String {
return this.getDisplayName(context, this.singleValue)
}
open fun getDisplayName(context: Context, value: T): String {
val prefix = this.resId?.let { val prefix = this.resId?.let {
context.getString(it) context.getString(it) + " "
} ?: "" } ?: ""
return prefix + " " + labelForValue(value, context) return prefix + labelForValue(value, context)
} }
} }
abstract class ListOfDouble : ListOfValues<Double>() { abstract class ListOfDouble : ListOfValues<Double>() {
open var sign: Int = 1 open var sign: Int = 1
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<Double>() override var listOfValues = mutableListOf<Double>()
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues() listOfValues = filterCondition.getValues()
@ -262,7 +224,6 @@ sealed class QueryCondition : RowRepresentable {
abstract class ListOfString : ListOfValues<String>() { abstract class ListOfString : ListOfValues<String>() {
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<String>() override var listOfValues = mutableListOf<String>()
override fun labelForValue(value: String, context: Context): String { override fun labelForValue(value: String, context: Context): String {
return value return value
} }
@ -288,7 +249,6 @@ sealed class QueryCondition : RowRepresentable {
} }
abstract class SingleInt(value: Int) : SingleValue<Int>(value) { abstract class SingleInt(value: Int) : SingleValue<Int>(value) {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return value.toString() return value.toString()
} }
@ -300,6 +260,13 @@ sealed class QueryCondition : RowRepresentable {
} }
override fun getDisplayName(context: Context): String {
this.resId?.let {
return context.getString(it)
}
return baseId
}
// override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament // override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament
abstract class QueryDataCondition<T : NameManageable> : ListOfString() { abstract class QueryDataCondition<T : NameManageable> : ListOfString() {
@ -324,10 +291,6 @@ sealed class QueryCondition : RowRepresentable {
return completeLabel return completeLabel
} }
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context)
}
open fun entityName(realm: Realm, context: Context): String { open fun entityName(realm: Realm, context: Context): String {
return entityName(context) return entityName(context)
} }
@ -336,12 +299,6 @@ sealed class QueryCondition : RowRepresentable {
val query = realm.where(entity) val query = realm.where(entity)
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 {
@ -478,20 +435,10 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
class AnyStake : ListOfString() { class AnyBlind : ListOfString() {
override fun labelForValue(value: String, context: Context): String {
return StakesHolder.readableStakes(value)
}
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
return context.getString(R.string.stakes) return context.getString(R.string.blinds)
} }
override fun compareTo(other: ListOfValues<String>): Int {
return CodedStake(this.listOfValues.first()).compareTo(CodedStake(other.listOfValues.first()))
}
} }
class NumberOfTable : ListOfInt() { class NumberOfTable : ListOfInt() {
@ -545,7 +492,7 @@ sealed class QueryCondition : RowRepresentable {
class TournamentNumberOfPlayer : ListOfInt() { class TournamentNumberOfPlayer : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return value.toString() + " " + context.getString(R.string.players).toLowerCase() return value.toString() + " " + context.getString(R.string.number_of_players)
} }
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
@ -554,19 +501,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() {
@ -614,9 +561,15 @@ sealed class QueryCondition : RowRepresentable {
override var operator = Operator.EQUALS override var operator = Operator.EQUALS
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
override fun getDisplayNameWithValues(context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return context.getString(R.string.period_in_days_with_value, this.singleValue.toString()) return value.toString()
} }
// override fun entityName(context: Context): String {
// return this.resId?.let {
// " " + context.getString(it)
// } ?: ""
// }
} }
class Duration(value: Int) : SingleInt(value) { class Duration(value: Int) : SingleInt(value) {
@ -645,14 +598,14 @@ sealed class QueryCondition : RowRepresentable {
object DateNotNull : NotNullQueryCondition() object DateNotNull : NotNullQueryCondition()
object EndDateNotNull : NotNullQueryCondition() object EndDateNotNull : NotNullQueryCondition()
object BiggestBetNotNull : NotNullQueryCondition() object BigBlindNotNull : 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 +701,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 +794,6 @@ sealed class QueryCondition : RowRepresentable {
} }
return realmQuery return realmQuery
} }
else -> {}
} }
if (this is CustomFieldRelated) { if (this is CustomFieldRelated) {
@ -877,22 +822,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 +835,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 -> {
@ -974,14 +891,15 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
override val viewType: Int override val viewType: Int
get() { get() {
return when (this) { return when (this) {
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 +912,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 +940,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 +970,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
} }
} }

@ -6,6 +6,7 @@ import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber import timber.log.Timber
import java.util.*
class HandSetup { class HandSetup {
@ -35,31 +36,9 @@ class HandSetup {
} }
var type: Session.Type? = null
var blinds: String? = null
// var bigBlind: Double? = null
var ante: Double? = null
var tableSize: Int? = null
var bigBlindAnte: Boolean = false
var game: Game? = null
var session: Session? = null
var straddlePositions: MutableList<Position> = mutableListOf()
private set
fun clearStraddles() {
this.straddlePositions.clear()
}
private fun configure(handHistory: HandHistory) { private fun configure(handHistory: HandHistory) {
this.blinds = handHistory.blinds this.smallBlind = handHistory.smallBlind
this.bigBlind = handHistory.bigBlind
this.bigBlindAnte = handHistory.bigBlindAnte this.bigBlindAnte = handHistory.bigBlindAnte
this.ante = handHistory.ante this.ante = handHistory.ante
this.tableSize = handHistory.numberOfPlayers this.tableSize = handHistory.numberOfPlayers
@ -77,15 +56,32 @@ class HandSetup {
this.game = session.game // we don't want to force the max number of cards if unsure this.game = session.game // we don't want to force the max number of cards if unsure
} }
this.type = session.sessionType this.type = session.sessionType
this.blinds = session.cgBlinds this.smallBlind = session.cgSmallBlind
this.ante = session.cgAnte this.bigBlind = session.cgBigBlind
this.tableSize = session.tableSize this.tableSize = session.tableSize
}
val blindValues = session.blindValues var type: Session.Type? = null
if (blindValues.size > 2) {
this.straddlePositions = Position.positionsPerPlayers(10).drop(2).take(blindValues.size - 2).toMutableList() var smallBlind: Double? = null
}
var bigBlind: Double? = null
var ante: Double? = null
var tableSize: Int? = null
var bigBlindAnte: Boolean = false
var game: Game? = null
var session: Session? = null
var straddlePositions: MutableList<Position> = mutableListOf()
private set
fun clearStraddles() {
this.straddlePositions.clear()
} }
/*** /***

@ -1,189 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.toCurrency
import java.lang.Integer.min
import java.text.NumberFormat
import java.text.ParseException
import java.util.*
data class CodedStake(var stakes: String) : Comparable<CodedStake> {
var ante: Double? = null
var blinds: String? = null
var currency: Currency
init {
var currencyCode: String? = null
val parameters = this.stakes.split(StakesHolder.cbSeparator)
parameters.forEach { param ->
when {
param.contains(StakesHolder.cbAnte) -> ante = param.removePrefix(StakesHolder.cbAnte).let { NumberFormat.getInstance().parse(it)?.toDouble() }
param.contains(StakesHolder.cbBlinds) -> blinds = param.removePrefix(StakesHolder.cbBlinds)
param.contains(StakesHolder.cbCode) -> currencyCode = param.removePrefix(
StakesHolder.cbCode
)
}
}
this.currency = currencyCode?.let { Currency.getInstance(it) }
?: run { UserDefaults.currency }
}
override fun compareTo(other: CodedStake): Int {
if (this.currency == other.currency) {
this.blinds?.let { b1 ->
other.blinds?.let { b2 ->
if (b1 == b2) {
return this.compareAnte(other)
} else {
val bv1 = this.reversedBlindsArray(b1)
val bv2 = this.reversedBlindsArray(b2)
for (i in 0 until min(bv1.size, bv2.size)) {
if (bv1[i] != bv2[i]) {
return bv1[i].compareTo(bv2[i])
} else {
continue
}
}
return bv1.size.compareTo(bv2.size)
}
} ?: run {
return 1
}
} ?: run {
return this.compareAnte(other)
}
} else {
return this.currency.currencyCode.compareTo(other.currency.currencyCode)
}
}
private fun compareAnte(other: CodedStake): Int {
this.ante?.let { a1 ->
other.ante?.let { a2 ->
return a1.compareTo(a2)
} ?: run {
return 1
}
} ?: run {
return -1
}
}
private fun reversedBlindsArray(blinds: String): List<Double> {
return blinds.split(BLIND_SEPARATOR).mapNotNull { NumberFormat.getInstance().parse(it)?.toDouble() }.reversed()
}
fun formattedStakes(): String {
val components = arrayListOf<String>()
this.formattedBlinds()?.let { components.add(it) }
if ((this.ante ?: -1.0) > 0.0) {
this.formattedAnte()?.let { components.add("($it)") }
}
return if (components.isNotEmpty()) {
components.joinToString(" ")
} else {
NULL_TEXT
}
}
private fun formattedBlinds(): String? {
this.blinds?.let {
val placeholder = 1.0
val regex = Regex("-?\\d+(\\.\\d+)?")
return placeholder.toCurrency(currency).replace(regex, it)
}
return null
}
private fun formattedAnte(): String? {
this.ante?.let {
return it.toCurrency(this.currency)
}
return null
}
}
interface StakesHolder {
companion object {
const val cbSeparator = ";"
const val cbAnte = "A="
const val cbBlinds = "B="
const val cbCode = "C="
fun readableStakes(value: String): String {
return CodedStake(value).formattedStakes()
}
}
val ante: Double?
val blinds: String?
val biggestBet: Double?
val stakes: String?
val bankroll: Bankroll?
fun setHolderStakes(stakes: String?)
fun setHolderBiggestBet(biggestBet: Double?)
val blindValues: List<Double>
get() {
this.blinds?.let { blinds ->
val blindsSplit = blinds.split(BLIND_SEPARATOR)
return blindsSplit.mapNotNull {
try {
NumberFormat.getInstance().parse(it)?.toDouble()
} catch (e: ParseException) {
null
}
}
}
return listOf()
}
fun generateStakes() {
if (this.ante == null && this.blinds == null) {
setHolderStakes(null)
return
}
val components = arrayListOf<String>()
this.blinds?.let { components.add("${cbBlinds}${it}") }
this.ante?.let { components.add("${cbAnte}${it.formatted}") }
val code = this.bankroll?.currency?.code ?: UserDefaults.currency.currencyCode
components.add("${cbCode}${code}")
setHolderStakes(components.joinToString(cbSeparator))
}
fun defineHighestBet() {
val bets = arrayListOf<Double>()
this.ante?.let { bets.add(it) }
bets.addAll(this.blindValues)
setHolderBiggestBet(bets.maxOrNull())
}
}

@ -2,34 +2,22 @@ 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.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.SessionSetManager import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import java.text.NumberFormat
class Patcher { 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
// with each other
patchMissingTransactionTypes(context) patchMissingTransactionTypes(context)
Preferences.executeOnce(Preferences.Keys.PATCH_COMPUTABLE_RESULTS, context) { patchSessionSet()
patchComputableResults()
}
Preferences.executeOnce(Preferences.Keys.PATCH_SESSION_SETS, context) { Preferences.executeOnce(Preferences.Keys.PATCH_SESSION_SETS, context) {
patchSessionSet() patchSessionSet()
} }
@ -39,42 +27,51 @@ class Patcher {
Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) { Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) {
patchDefaultTransactionTypes(context) patchDefaultTransactionTypes(context)
} }
Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) { Preferences.executeOnce(Preferences.Keys.PATCH_BLINDS_FORMAT, context) {
patchStakes() patchBlindFormat()
}
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) {
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)
realm.executeTransaction { .equalTo("kind", depositType.uniqueIdentifier).findFirst()
Seed.createDefaultTransactionTypes(transactionTypes, context, realm) if (deposit == null) {
realm.executeTransaction {
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() {
@ -89,8 +86,7 @@ class Patcher {
it.computeStats() it.computeStats()
} }
sessions.forEach { sessions.forEach {
it.generateStakes() it.formatBlinds()
it.defineHighestBet()
} }
results.forEach { results.forEach {
it.computeNumberOfRebuy() it.computeNumberOfRebuy()
@ -115,50 +111,13 @@ class Patcher {
realm.close() realm.close()
} }
private fun patchStakes() { private fun patchBlindFormat() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransaction {
val sessions = realm.where(Session::class.java).findAll() val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session -> sessions.forEach { session ->
val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull() session.formatBlinds()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
session.cgAnte = null
if (blindsFormatted.isNotEmpty()) {
session.cgBlinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
} }
val handHistories = realm.where(HandHistory::class.java).findAll()
handHistories.forEach { hh ->
val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
if (blindsFormatted.isNotEmpty()) {
hh.blinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
}
}
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() realm.close()
} }
@ -180,55 +139,6 @@ class Patcher {
realm.close() realm.close()
} }
/*
15/04/21: Two needs:
- To get better performance for ITM Ratio, a positive session is a net >= 0 for cash game, or a cashedOut > 0 for tournaments
- The field "ratedTips" is added
*/
private fun patchComputableResults() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val crs = realm.where(ComputableResult::class.java).findAll()
crs.forEach { cr ->
cr.session?.let { cr.updateWith(it) }
}
}
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()
}
} }
} }

@ -9,7 +9,7 @@ import java.util.*
class PokerAnalyticsMigration : RealmMigration { class PokerAnalyticsMigration : RealmMigration {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
// DynamicRealm exposes an editable schema // DynamicRealm exposes an editable schema
val schema = realm.schema val schema = realm.schema
@ -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")
@ -234,115 +227,13 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++ currentVersion++
} }
// Migrate to version 10
if (currentVersion == 9) {
schema.get("Session")?.addField("handsCount", Int::class.java)
?.setRequired("handsCount", false)
schema.get("Session")?.transform { obj -> // otherwise we get a 0 default value
obj.setNull("handsCount")
}
val configSchema = schema.create("UserConfig")
configSchema.addField("id", String::class.java).setRequired("id", true)
configSchema.addPrimaryKey("id")
configSchema.addField("liveDealtHandsPerHour", Int::class.java)
configSchema.addField("onlineDealtHandsPerHour", Int::class.java)
currentVersion++
}
// Migrate to version 11
if (currentVersion == 10) {
schema.get("ComputableResult")?.addField("ratedTips", Double::class.java)
currentVersion++
}
// Migrate to version 12
if (currentVersion == 11) {
schema.get("Session")?.let { ss ->
ss.addField("cgAnte", Double::class.java)
?.setNullable("cgAnte", true)
ss.addField("cgBiggestBet", Double::class.java)
?.setNullable("cgBiggestBet", true)
ss.addField("cgStakes", String::class.java)
ss.addField("cgBlinds", String::class.java)
ss.removeField("blinds")
ss.renameField("cgSmallBlind", "cgOldSmallBlind")
ss.renameField("cgBigBlind", "cgOldBigBlind")
}
schema.get("HandHistory")?.let { hs ->
hs.setNullable("ante", true)
hs.addField("stakes", String::class.java)
hs.addField("blinds", String::class.java)
hs.addField("biggestBet", Double::class.java)
?.setNullable("biggestBet", true)
hs.renameField("smallBlind", "oldSmallBlind")
hs.renameField("bigBlind", "oldBigBlind")
}
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 {
return other is RealmMigration
} }
override fun hashCode(): Int { override fun equals(other: Any?): Boolean {
return RealmMigration::javaClass.hashCode() return other is RealmMigration
} }
override fun hashCode(): Int {
return RealmMigration::javaClass.hashCode()
}
} }

@ -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

@ -4,7 +4,7 @@ import io.realm.RealmObject
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
open class ComputableResult : RealmObject(), Filterable { open class ComputableResult() : RealmObject(), Filterable {
var ratedNet: Double = 0.0 var ratedNet: Double = 0.0
@ -22,8 +22,6 @@ open class ComputableResult : RealmObject(), Filterable {
var session: Session? = null var session: Session? = null
var ratedTips: Double = 0.0
fun updateWith(session: Session) { fun updateWith(session: Session) {
val rate = session.bankroll?.currency?.rate ?: 1.0 val rate = session.bankroll?.currency?.rate ?: 1.0
@ -32,11 +30,10 @@ open class ComputableResult : RealmObject(), Filterable {
this.ratedNet = result.net * rate this.ratedNet = result.net * rate
this.isPositive = result.isPositive this.isPositive = result.isPositive
this.ratedBuyin = (result.buyin ?: 0.0) * rate this.ratedBuyin = (result.buyin ?: 0.0) * rate
this.ratedTips = (result.tips ?: 0.0) * rate
} }
this.bbNet = session.bbNet this.bbNet = session.bbNet
this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0 this.hasBigBlind = if (session.cgBigBlind != null) 1 else 0
this.estimatedHands = session.estimatedHands this.estimatedHands = session.estimatedHands
this.bbPer100Hands = this.bbPer100Hands =
session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
@ -50,7 +47,6 @@ open class ComputableResult : RealmObject(), Filterable {
IS_POSITIVE("isPositive"), IS_POSITIVE("isPositive"),
RATED_BUYIN("ratedBuyin"), RATED_BUYIN("ratedBuyin"),
ESTIMATED_HANDS("estimatedHands"), ESTIMATED_HANDS("estimatedHands"),
RATED_TIPS("ratedTips"),
// BB_PER100HANDS("bbPer100Hands") // BB_PER100HANDS("bbPer100Hands")
} }

@ -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

@ -75,10 +75,6 @@ open class Result : RealmObject(), Filterable {
* Tips * Tips
*/ */
var tips: Double? = null var tips: Double? = null
set(value) {
field = value
this.session?.computeStats()
}
// The transactions associated with the Result, impacting the result // The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList() var transactions: RealmList<Transaction> = RealmList()
@ -102,19 +98,13 @@ open class Result : RealmObject(), Filterable {
/** /**
* Returns 1 if the session is positive * Returns 1 if the session is positive
*/ */
val isPositive: Int @Ignore
get() { val isPositive: Int = if (this.net >= 0.0) 1 else 0
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
} else {
if (this.net >= 0.0) 1 else 0
}
}
// 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
@ -150,7 +140,7 @@ open class Result : RealmObject(), Filterable {
fun computeNumberOfRebuy() { fun computeNumberOfRebuy() {
this.session?.let { this.session?.let {
if (it.isCashGame()) { if (it.isCashGame()) {
it.cgBiggestBet?.let { bb -> it.cgBigBlind?.let { bb ->
if (bb > 0.0) { if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0) this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else { } else {

@ -16,7 +16,6 @@ import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.Stakes
import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.extensions.SessionState import net.pokeranalytics.android.model.extensions.SessionState
@ -27,26 +26,24 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.filter.QueryCondition.* import net.pokeranalytics.android.model.filter.QueryCondition.*
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.model.utils.SessionSetManager import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.hourMinute import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.extensions.shortDateTime import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.toCurrency import net.pokeranalytics.android.util.extensions.*
import net.pokeranalytics.android.util.extensions.toMinutes
import java.text.DateFormat import java.text.DateFormat
import java.text.NumberFormat
import java.text.ParseException
import java.util.* import java.util.*
import java.util.Currency import java.util.Currency
typealias BB = Double typealias BB = Double
open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Timed, open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Timed,
TimeFilterable, Filterable, DatedBankrollGraphEntry, StakesHolder { TimeFilterable, Filterable, DatedBankrollGraphEntry {
enum class Type(val value: String) { enum class Type(val value: String) {
CASH_GAME("Cash Game"), CASH_GAME("Cash Game"),
@ -77,9 +74,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal
session.limit = Limit.NO.ordinal
session.game = realm.where(Game::class.java).equalTo("shortName", "HE").findFirst()
return if (managed) { return if (managed) {
realm.copyToRealm(session) realm.copyToRealm(session)
} else { } else {
@ -99,7 +93,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
AnyLimit::class.java -> "limit" AnyLimit::class.java -> "limit"
AnyTableSize::class.java -> "tableSize" AnyTableSize::class.java -> "tableSize"
AnyTournamentType::class.java -> "tournamentType" AnyTournamentType::class.java -> "tournamentType"
AnyStake::class.java -> "cgStakes" AnyBlind::class.java -> "blinds"
NumberOfTable::class.java -> "numberOfTables" NumberOfTable::class.java -> "numberOfTables"
NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet" NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet"
NumberOfRebuy::class.java -> "result.numberOfRebuy" NumberOfRebuy::class.java -> "result.numberOfRebuy"
@ -110,9 +104,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek" AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek"
AnyMonthOfYear::class.java -> "month" AnyMonthOfYear::class.java -> "month"
AnyYear::class.java -> "year" AnyYear::class.java -> "year"
PastDay::class.java, IsToday::class.java, WasYesterday::class.java, PastDay::class.java, IsToday::class.java, WasYesterday::class.java, WasTodayAndYesterday::class.java, DuringThisYear::class.java, DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate"
WasTodayAndYesterday::class.java, DuringThisYear::class.java,
DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate"
StartedFromTime::class.java -> "startDateHourMinuteComponent" StartedFromTime::class.java -> "startDateHourMinuteComponent"
EndedToTime::class.java -> "endDateHourMinuteComponent" EndedToTime::class.java -> "endDateHourMinuteComponent"
Duration::class.java -> "netDuration" Duration::class.java -> "netDuration"
@ -121,7 +113,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
CustomFieldQuery::class.java -> "customFieldEntries.customFields.id" CustomFieldQuery::class.java -> "customFieldEntries.customFields.id"
DateNotNull::class.java -> "startDate" DateNotNull::class.java -> "startDate"
EndDateNotNull::class.java -> "endDate" EndDateNotNull::class.java -> "endDate"
BiggestBetNotNull::class.java -> "cgBiggestBet" BigBlindNotNull::class.java -> "cgBigBlind"
else -> null else -> null
} }
} }
@ -267,20 +259,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
override var bankroll: Bankroll? = null override var bankroll: Bankroll? = null
set(value) { set(value) {
field = value field = value
this.generateStakes() this.formatBlinds()
this.computeStats()
// this.updateRowRepresentation() // this.updateRowRepresentation()
} }
// 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
@ -293,12 +277,6 @@ 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) {
if (value > 0) {
field = value
this.computeStats()
}
}
// The hand histories of the session // The hand histories of the session
@LinkingObjects("session") @LinkingObjects("session")
@ -313,43 +291,23 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// Cash Game // Cash Game
// The small blind value // The small blind value
var cgOldSmallBlind: Double? = null var cgSmallBlind: Double? = null
set(value) { set(value) {
field = value field = value
formatBlinds()
} }
// The big blind value // The big blind value
var cgOldBigBlind: Double? = null var cgBigBlind: Double? = null
set(value) {
field = value
this.computeStats()
this.result?.computeNumberOfRebuy()
}
// var blinds: String? = null
// private set
var cgAnte: Double? = null
set(value) { set(value) {
field = value field = value
this.generateStakes()
this.defineHighestBet()
this.computeStats()
this.result?.computeNumberOfRebuy()
}
var cgBlinds: String? = null
set(value) {
field = cleanupBlinds(value)
this.generateStakes()
this.defineHighestBet()
this.computeStats() this.computeStats()
formatBlinds()
this.result?.computeNumberOfRebuy() this.result?.computeNumberOfRebuy()
} }
var cgBiggestBet: Double? = null var blinds: String? = null
private set
var cgStakes: String? = null
// Tournament // Tournament
@ -375,15 +333,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The custom fields values // The custom fields values
var customFieldEntries: RealmList<CustomFieldEntry> = RealmList() var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()
// The number of hands played during the sessions
var handsCount: Int? = null
set(value) {
field = value
this.computeStats()
}
fun bankrollHasBeenUpdated() { fun bankrollHasBeenUpdated() {
this.generateStakes() formatBlinds()
} }
/** /**
@ -429,12 +380,17 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// Stats // Stats
@Ignore
val ONLINE_PLAYER_HANDS_PER_HOUR = 500.0
@Ignore
val LIVE_PLAYER_HANDS_PER_HOUR = 250.0
/** /**
* The net result in big blinds * The net result in big blinds
*/ */
val bbNet: BB val bbNet: BB
get() { get() {
val bb = this.cgBiggestBet val bb = this.cgBigBlind
val result = this.result val result = this.result
return if (bb != null && result != null) { return if (bb != null && result != null) {
result.net / bb result.net / bb
@ -449,14 +405,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
@Ignore @Ignore
var estimatedHands: Double = 0.0 var estimatedHands: Double = 0.0
get() { get() {
this.handsCount?.let {
return it.toDouble()
}
val noh = this.numberOfHandsPerHour val noh = this.numberOfHandsPerHour
val hd = this.hourlyDuration val hd = this.hourlyDuration
return noh * hd return noh * hd
} }
// DatedValue // DatedValue
@Ignore @Ignore
@ -501,9 +455,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
val numberOfHandsPerHour: Double val numberOfHandsPerHour: Double
get() { get() {
val tableSize = this.tableSize ?: 9 // 9 is the default table size if null val tableSize = this.tableSize ?: 9 // 9 is the default table size if null
val config = UserConfig.getConfiguration(this.realm) val playerHandsPerHour = if (this.isLive) LIVE_PLAYER_HANDS_PER_HOUR else ONLINE_PLAYER_HANDS_PER_HOUR
val playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour return playerHandsPerHour / tableSize.toDouble()
return this.numberOfTables * playerHandsPerHour / tableSize.toDouble()
} }
val hourlyRate: Double val hourlyRate: Double
@ -675,8 +628,22 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT
} }
fun getFormattedStakes(): String { fun getFormattedBlinds(): String {
return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT } return blinds ?: 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 +653,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()
@ -712,15 +679,14 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
fun duplicate(): Session { fun duplicate() : Session {
val copy = newInstance(this.realm, this.isTournament(), this.bankroll) val copy = newInstance(this.realm, this.isTournament(), this.bankroll)
copy.game = this.game copy.game = this.game
copy.limit = this.limit copy.limit = this.limit
copy.cgBlinds = this.cgBlinds copy.cgSmallBlind = this.cgSmallBlind
copy.cgAnte = this.cgAnte copy.cgBigBlind = this.cgBigBlind
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
@ -742,16 +708,27 @@ 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.BLINDS -> if (value is ArrayList<*>) {
if (value.ante != null) { cgSmallBlind = try {
this.cgAnte = value.ante (value[0] as String? ?: "0").toDouble()
} catch (e: Exception) {
null
} }
if (value.blinds != null) {
this.cgBlinds = value.blinds 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 cgSmallBlind = null
this.cgAnte = null cgBigBlind = null
} }
SessionPropertiesRow.BREAK_TIME -> { SessionPropertiesRow.BREAK_TIME -> {
this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000 this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000
@ -759,6 +736,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()
@ -830,8 +808,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
tournamentFeatures.removeAll(this.tournamentFeatures) tournamentFeatures.removeAll(this.tournamentFeatures)
} }
} }
SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt()
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables = (value as Double?)?.toInt() ?: 1
is CustomField -> { is CustomField -> {
customFieldEntries.filter { it.customField?.id == row.id }.let { customFieldEntries.filter { it.customField?.id == row.id }.let {
customFieldEntries.removeAll(it) customFieldEntries.removeAll(it)
@ -897,7 +873,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
@ -969,7 +945,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
return when (row) { return when (row) {
SessionPropertiesRow.BANKROLL -> bankroll?.name ?: NULL_TEXT SessionPropertiesRow.BANKROLL -> bankroll?.name ?: NULL_TEXT
SessionPropertiesRow.STAKES -> getFormattedStakes() SessionPropertiesRow.BLINDS -> getFormattedBlinds()
SessionPropertiesRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT SessionPropertiesRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
@ -1006,8 +982,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT
SessionPropertiesRow.HANDS -> this.handHistories?.size.toString() SessionPropertiesRow.HANDS -> this.handHistories?.size.toString()
SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context)
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString()
is CustomField -> { is CustomField -> {
customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry -> customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(currency) return customFieldEntry.getFormattedValue(currency)
@ -1018,11 +992,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
} }
private fun handsCountFormatted(context: Context): String {
return this.handsCount?.toString() ?:
"${estimatedHands.toInt()} (${context.getString(R.string.estimated)})"
}
fun clearBuyinCashedOut() { fun clearBuyinCashedOut() {
this.result?.buyin = null this.result?.buyin = null
this.result?.cashout = null this.result?.cashout = null
@ -1032,46 +1001,4 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.result?.netResult = null this.result?.netResult = null
} }
private fun cleanupBlinds(blinds: String?): String? {
if (blinds == null) {
return null
}
val blindValues = blinds.split(BLIND_SEPARATOR).mapNotNull {
try {
NumberFormat.getInstance().parse(it)
} catch (e: ParseException) {
null
}
}
return if (blindValues.isNotEmpty()) {
blindValues.joinToString(BLIND_SEPARATOR)
} else {
null
}
}
/// StakesHolder
override val ante: Double?
get() { return this.cgAnte }
override val blinds: String?
get() { return this.cgBlinds }
override val biggestBet: Double?
get() { return this.cgBiggestBet }
override val stakes: String?
get() { return this.cgStakes }
override fun setHolderStakes(stakes: String?) {
this.cgStakes = stakes
}
override fun setHolderBiggestBet(biggestBet: Double?) {
this.cgBiggestBet = biggestBet
}
} }

@ -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,16 +34,16 @@ 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
transaction.amount = abs(amount) * -1
}
return transaction 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? {
return when (queryCondition) { return when (queryCondition) {
@ -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) {

@ -1,41 +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.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
open class UserConfig : RealmObject() {
companion object {
fun getConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config
}
return UserConfig()
}
}
@PrimaryKey
var id = UUID.randomUUID().toString()
var liveDealtHandsPerHour: Int = 250
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) }
}
}

@ -14,7 +14,6 @@ import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
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 timber.log.Timber
interface CardProperty interface CardProperty
@ -25,10 +24,11 @@ fun List<Card>.formatted(context: Context) : CharSequence? {
var span: CharSequence? = null var span: CharSequence? = null
this.forEach { this.forEach {
val formatted = it.formatted(context) val formatted = it.formatted(context)
span = if (span == null) { if (span == null) {
formatted span = formatted
} else { }
TextUtils.concat(span, formatted) else {
span = TextUtils.concat(span, formatted)
} }
} }
return span return span
@ -95,10 +95,6 @@ open class Card : RealmObject() {
companion object { companion object {
val baseSuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER)
}
val displaySuits: List<Suit> by lazy { val displaySuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED) listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED)
} }
@ -244,49 +240,9 @@ open class Card : RealmObject() {
} }
} }
fun copy(): Card {
val card = Card()
card.value = this.value
card.suit = this.suit
return card
}
val isWildCard: Boolean val isWildCard: Boolean
get() { get() {
return this.value == null || this.isSuitWildCard return this.value == null || this.suit == null || this.suit == Suit.UNDEFINED
}
val isSuitWildCard: Boolean
get() {
return this.suit == null || this.suit == Suit.UNDEFINED
}
}
fun List<Card>.putSuits(usedCards: MutableList<Card>) : List<Card> {
fun getLeastUsedValidSuit(value: Int, usedCards: MutableList<Card>, addedCards: List<Card>): Card.Suit {
val availableSuits = Card.Suit.baseSuits.toMutableList()
usedCards.filter { it.value == value }.forEach { availableSuits.remove(it.suit) }
val currentCards = this.drop(addedCards.size) + addedCards
val cardsPerSuit = availableSuits.associateWith { suit ->
currentCards.filter { it.suit == suit }
} }
return cardsPerSuit.minByOrNull { it.value.count() }!!.key
}
val list = mutableListOf<Card>()
this.forEach { cardToClone ->
val card = cardToClone.copy()
if (card.value != null && card.isSuitWildCard) {
card.suit = getLeastUsedValidSuit(card.value!!, usedCards, list)
usedCards.add(card)
Timber.d("Selected suit for wildcard: ${card.suit?.value}, value = ${card.value}")
}
list.add(card)
}
return list
} }

@ -15,8 +15,10 @@ import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.handhistory.HandSetup import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.TimeFilterable
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow
@ -26,14 +28,13 @@ import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.addLineReturn import net.pokeranalytics.android.util.extensions.addLineReturn
import net.pokeranalytics.android.util.extensions.formatted import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.fullDate import net.pokeranalytics.android.util.extensions.fullDate
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean) data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean)
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable, open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable,
CardHolder, Comparator<PositionAmount>, StakesHolder { CardHolder, Comparator<PositionAmount> {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -62,67 +63,34 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/*** /***
* The small blind * The small blind
*/ */
var oldSmallBlind: Double? = null var smallBlind: Double? = null
set(value) { set(value) {
field = value field = value
// if (this.bigBlind == null && value != null) { if (this.bigBlind == null && value != null) {
// this.bigBlind = value * 2 this.bigBlind = value * 2
// } }
} }
/*** /***
* The big blind * The big blind
*/ */
var oldBigBlind: Double? = null var bigBlind: Double? = null
set(value) { set(value) {
field = value field = value
// if (this.smallBlind == null && value != null) { if (this.smallBlind == null && value != null) {
// this.smallBlind = value / 2 this.smallBlind = value / 2
// } }
} }
/***
* Big blind ante
*/
var bigBlindAnte: Boolean = false
/*** /***
* The ante * The ante
*/ */
override var ante: Double? = 0.0 var ante: Double = 0.0
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
/***
* The blinds
*/
override var blinds: String? = null
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
override var biggestBet: Double? = null
/*** /***
* The coded stakes * Big blind ante
*/ */
override var stakes: String? = null var bigBlindAnte: Boolean = false
override val bankroll: Bankroll?
get() { return this.session?.bankroll }
override fun setHolderStakes(stakes: String?) {
this.stakes = stakes
}
override fun setHolderBiggestBet(biggestBet: Double?) {
this.biggestBet = biggestBet
}
/*** /***
* Number of players in the hand * Number of players in the hand
@ -190,15 +158,13 @@ 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.smallBlind?.let { this.smallBlind = it }
handSetup.blinds?.let { this.blinds = it } handSetup.bigBlind?.let { this.bigBlind = it }
this.session = handSetup.session this.session = handSetup.session
this.date = this.session?.handHistoryAutomaticDate ?: Date() this.date = this.session?.handHistoryAutomaticDate ?: Date()
@ -213,32 +179,18 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.actions.clear() this.actions.clear()
var blindValues = this.blindValues this.addAction(0, Action.Type.POST_SB, this.smallBlind)
if (blindValues.isNotEmpty()) { this.addAction(1, Action.Type.POST_BB, this.bigBlind)
blindValues.forEachIndexed { index, blind ->
val action = when(index) { // var lastStraddler: Int? = null
0 -> Action.Type.POST_SB
1 -> Action.Type.POST_BB
else -> null
}
action?.let { this.addAction(index, action, blind) }
}
} else {
this.addAction(0, Action.Type.POST_SB, this.oldSmallBlind)
this.addAction(1, Action.Type.POST_BB, this.oldBigBlind)
}
blindValues = blindValues.drop(2)
val positions = Position.positionsPerPlayers(this.numberOfPlayers) val positions = Position.positionsPerPlayers(this.numberOfPlayers)
handSetup.straddlePositions.forEachIndexed { index, position -> // position are sorted here handSetup.straddlePositions.forEach { position -> // position are sorted here
val positionIndex = positions.indexOf(position) val positionIndex = positions.indexOf(position)
val amount = if (index < blindValues.size) { blindValues[index] } else null this.addAction(positionIndex, Action.Type.STRADDLE)
this.addAction(positionIndex, Action.Type.STRADDLE, amount) // lastStraddler = positionIndex
} }
// var lastStraddler: Int? = null
// val totalActions = this.actions.size // val totalActions = this.actions.size
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions // val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions
@ -284,9 +236,9 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val anteSum: Double val anteSum: Double
get() { get() {
return if (bigBlindAnte) { return if (bigBlindAnte) {
this.biggestBet ?: 0.0 this.bigBlind ?: 0.0
} else { } else {
(this.ante ?: 0.0) * this.numberOfPlayers this.ante * this.numberOfPlayers
} }
} }
@ -334,7 +286,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
@ -350,20 +302,14 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}" val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}"
val firstLineComponents = mutableListOf(this.date.fullDate(), players) val firstLineComponents = mutableListOf(this.date.fullDate(), players)
this.blinds?.let { firstLineComponents.add(it) } this.smallBlind?.let { sb ->
this.bigBlind?.let { bb ->
// this.smallBlind?.let { sb -> firstLineComponents.add("${sb.formatted}/${bb.formatted}")
// this.bigBlind?.let { bb ->
// firstLineComponents.add("${sb.formatted}/${bb.formatted}")
// }
// }
this.ante?.let {
if (it > 0.0) {
firstLineComponents.add("ante ${this.ante}")
} }
} }
if (this.ante > 0.0) {
firstLineComponents.add("ante ${this.ante}")
}
string = string.plus(firstLineComponents.joinToString(" - ")) string = string.plus(firstLineComponents.joinToString(" - "))
string = string.addLineReturn(2) string = string.addLineReturn(2)
@ -382,26 +328,26 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
// Actions per street // Actions per street
val sortedActions = this.actions.sortedBy { it.index } val sortedActions = this.actions.sortedBy { it.index }
// Remove SUMMARY // val actionList = ActionList()
Street.values().dropLast(1).forEach { street -> // actionList.load(this)
Street.values().forEach { street ->
string = string.addLineReturn(2) string = string.addLineReturn(2)
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex) val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex)
if (streetActions.isNotEmpty()) {
val streetCards = this.cardsForStreet(street)
// we want to show streets: with actions or when an allin is called
if (streetCards.size == street.totalBoardCards || streetActions.isNotEmpty()) {
val streetItems = mutableListOf<CharSequence>(context.getString(street.resId)) val streetItems = mutableListOf<CharSequence>(context.getString(street.resId))
val potSize = this.potSizeForStreet(street) val potSize = this.potSizeForStreet(street)
if (potSize > 0) { if (potSize > 0) {
// streetItems.add(context.getString(R.string.pot_size))
streetItems.add("(" + potSize.formatted + ")") streetItems.add("(" + potSize.formatted + ")")
} }
string = string.plus(streetItems.joinToString(" ")) string = string.plus(streetItems.joinToString(" "))
val streetCards = this.cardsForStreet(street)
if (streetCards.isNotEmpty()) { if (streetCards.isNotEmpty()) {
string = string.addLineReturn() string = string.addLineReturn()
string = string.plus(streetCards.formatted(context) ?: "") string = string.plus(streetCards.formatted(context) ?: "")
@ -415,7 +361,6 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
string = string.plus(localizedAction(action, context)) string = string.plus(localizedAction(action, context))
string = string.addLineReturn() string = string.addLineReturn()
} }
} }
} }
@ -426,12 +371,12 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
fun anteForPosition(position: Position): Double { fun anteForPosition(position: Position): Double {
return if (this.bigBlindAnte) { return if (this.bigBlindAnte) {
if (position == Position.BB) { if (position == Position.BB) {
this.biggestBet ?: 0.0 this.bigBlind ?: 0.0
} else { } else {
0.0 0.0
} }
} else { } else {
this.ante ?: 0.0 this.ante
} }
} }
@ -447,11 +392,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroString = context.getString(R.string.hero) val heroString = context.getString(R.string.hero)
playerItems.add("- $heroString") playerItems.add("- $heroString")
} }
playerItems.add("[${playerSetup.cards.formatted(context)}]")
if (playerSetup.cards.isNotEmpty()) {
playerItems.add("[${playerSetup.cards.formatted(context)}]")
}
playerSetup.stack?.let { stack -> playerSetup.stack?.let { stack ->
playerItems.add("- $stack") playerItems.add("- $stack")
} }
@ -479,12 +420,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
} }
@ -568,8 +504,6 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.winnerPots.clear() this.winnerPots.clear()
this.winnerPots.addAll(wonPots) this.winnerPots.addAll(wonPots)
Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map {it.position}} ")
} }
/*** /***
@ -586,14 +520,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,22 +580,19 @@ 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
val share = pot.amount / winningPositions.size
// Distributes the pot for each winners winningPositions.forEach { p ->
val share = pot.amount / winningPositions.size val wp = wonPots[p]
winningPositions.forEach { p -> if (wp == null) {
val wp = wonPots[p] val wonPot = WonPot()
if (wp == null) { wonPot.position = p
val wonPot = WonPot() wonPot.amount = share
wonPot.position = p wonPots[p] = wonPot
wonPot.amount = share } else {
wonPots[p] = wonPot wp.amount += share
} else {
wp.amount += share
}
} }
} }
@ -675,32 +606,17 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
*/ */
private fun compareHands(activePositions: List<Int>): List<Int> { private fun compareHands(activePositions: List<Int>): List<Int> {
val usedFullCards = this.allFullCards.toMutableList()
val noWildCardBoard = this.board.putSuits(usedFullCards)
val noWildCardHands = activePositions.map {
this.playerSetupForPosition(it)?.cards?.putSuits(usedFullCards)
}
// Evaluate all hands // Evaluate all hands
val results = noWildCardHands.map { cards -> val results = activePositions.map {
cards?.let { hand -> this.playerSetupForPosition(it)?.cards?.let { hand ->
EvaluatorBridge.playerHand(hand, noWildCardBoard) EvaluatorBridge.playerHand(hand, this.board)
} ?: run { } ?: run {
Int.MAX_VALUE Int.MAX_VALUE
} }
} }
// Evaluate all hands
// val results = activePositions.map {
// this.playerSetupForPosition(it)?.cards?.let { hand ->
// EvaluatorBridge.playerHand(hand, this.board)
// } ?: run {
// Int.MAX_VALUE
// }
// }
// Check who has best score (EvaluatorBridge gives a lowest score for a better hand) // Check who has best score (EvaluatorBridge gives a lowest score for a better hand)
return results.minOrNull()?.let { best -> return results.min()?.let { best ->
val winners = mutableListOf<Int>() val winners = mutableListOf<Int>()
results.forEachIndexed { index, i -> results.forEachIndexed { index, i ->
if (i == best && i != Int.MAX_VALUE) { if (i == best && i != Int.MAX_VALUE) {
@ -743,33 +659,4 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return boardHasWildCard || playerCardHasWildCard return boardHasWildCard || playerCardHasWildCard
} }
private val allFullCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
this.playerSetups.forEach {
cards.addAll(it.cards)
}
return cards.filter { !it.isWildCard }
}
val allSuitWildCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
this.playerSetups.forEach {
cards.addAll(it.cards)
}
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
}
}
} }

@ -11,18 +11,15 @@ class DataUtils {
companion object { companion object {
/** /**
* Returns the number of sessions corresponding to the provided parameters * Returns true if the provided parameters doesn't correspond to an existing session
*/ */
fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int { fun sessionCount(realm: Realm, startDate: Date, endDate: Date, net: Double): Int {
var sessionQuery = realm.where(Session::class.java) val sessions = realm.where(Session::class.java)
.equalTo("startDate", startDate) .equalTo("startDate", startDate)
.equalTo("endDate", endDate)
.equalTo("result.net", net) .equalTo("result.net", net)
.findAll()
endDate?.let { return sessions.size
sessionQuery = sessionQuery.equalTo("endDate", it)
}
return sessionQuery.findAll().size
} }
/** /**

@ -39,7 +39,7 @@ private fun Session.significantFields(): List<SessionPropertiesRow> {
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {
return listOf( return listOf(
SessionPropertiesRow.GAME, SessionPropertiesRow.GAME,
SessionPropertiesRow.STAKES, SessionPropertiesRow.BLINDS,
SessionPropertiesRow.BANKROLL, SessionPropertiesRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE SessionPropertiesRow.TABLE_SIZE
) )
@ -75,7 +75,8 @@ class FavoriteSessionFinder {
*/ */
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) { fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) {
val favoriteSession = favoriteSession(session.type, location, session.realm, context) val favoriteSession =
favoriteSession(session.type, location, session.realm, context)
favoriteSession?.let { fav -> favoriteSession?.let { fav ->
@ -86,8 +87,8 @@ class FavoriteSessionFinder {
when (session.type) { when (session.type) {
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {
session.cgAnte = fav.cgAnte session.cgSmallBlind = fav.cgSmallBlind
session.cgBlinds = fav.cgBlinds session.cgBigBlind = fav.cgBigBlind
} }
Session.Type.TOURNAMENT.ordinal -> { Session.Type.TOURNAMENT.ordinal -> {
session.tournamentEntryFee = fav.tournamentEntryFee session.tournamentEntryFee = fav.tournamentEntryFee

@ -17,19 +17,15 @@ 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()
if (existing.isEmpty()) {
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll() val type = TransactionType()
if (existing.isEmpty()) { type.name = value.localizedTitle(context)
val type = TransactionType() type.additive = value.additive
type.name = value.localizedTitle(context) type.kind = value.uniqueIdentifier
type.additive = value.additive type.lock = true
type.kind = value.uniqueIdentifier realm.insertOrUpdate(type)
type.lock = true
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,14 +77,52 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
initUI() initUI()
checkFirstLaunch() checkFirstLaunch()
this.paApplication.reportWhistleBlower?.addListener(this)
} }
override fun onDestroy() { // override fun onNewIntent(intent: Intent?) {
super.onDestroy() // super.onNewIntent(intent)
this.paApplication.reportWhistleBlower?.removeListener(this) //
} // setIntent(intent)
// intent?.let {
//
// when (intent.action) {
// "android.intent.action.VIEW" -> { // import
// val data = it.data
// if (data != null) {
// this.requestImportConfirmation(data)
// } else {
// throw PAIllegalStateException("URI null on import")
// }
// }
// else -> {
// Timber.w("Intent ${intent.action} unmanaged")
// }
// }
// }
//
// }
// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// super.onActivityResult(requestCode, resultCode, data)
//
// when (requestCode) {
// RequestCode.IMPORT.value -> {
// if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) {
// showAlertDialog(context = this, message = R.string.unknown_import_format_popup_message)
// }
// }
// }
// }
// Import
// private fun requestImportConfirmation(uri: Uri) {
//
// showAlertDialog(context = this, title = R.string.import_confirmation, showCancelButton = true, positiveAction = {
// ImportActivity.newInstanceForResult(this, uri)
// })
//
// }
private fun observeRealmObjects() { private fun observeRealmObjects() {
@ -117,9 +130,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 +153,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 +172,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()
}
}
} }
} }

@ -9,7 +9,6 @@ import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Session 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.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -18,6 +17,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() {
@ -49,7 +49,7 @@ class ImportActivity : BaseActivity() {
intent?.data?.let { intent?.data?.let {
this.fileURI = it this.fileURI = it
} ?: run { } ?: run {
this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) ?: throw PAIllegalStateException("Uri not found") this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) as Uri
} }
setContentView(R.layout.activity_import) setContentView(R.layout.activity_import)
@ -63,13 +63,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 +82,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,12 @@ 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.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,13 +36,14 @@ 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)
} }
// var bottomSheetRow: RowRepresentable?
// get() { return this.bottomSheetViewModel.rowRepresentable }
// set(value) { this.bottomSheetViewModel.rowRepresentable = value }
fun setBottomSheetParameters(rowRepresentable: RowRepresentable, delegate: RowRepresentableDelegate) { fun setBottomSheetParameters(rowRepresentable: RowRepresentable, delegate: RowRepresentableDelegate) {
this.bottomSheetViewModel.rowRepresentable = rowRepresentable this.bottomSheetViewModel.rowRepresentable = rowRepresentable
this.bottomSheetViewModel.delegate = delegate this.bottomSheetViewModel.delegate = delegate
@ -58,14 +54,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() {
@ -89,11 +83,6 @@ abstract class BaseActivity : AppCompatActivity() {
this.realm?.close() this.realm?.close()
} }
override fun onBackPressed() {
super.onBackPressed()
AppReviewManager.showReviewManager(this)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@ -134,27 +123,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) {

@ -4,7 +4,6 @@ import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -137,7 +136,6 @@ open class MediaActivity : BaseActivity() {
selectedChoice = -1 selectedChoice = -1
} }
// Build.VERSION.SDK_INT < Build.VERSION_CODES.R &&
/** /**
* Open the Camera Intent * Open the Camera Intent
@ -149,40 +147,35 @@ open class MediaActivity : BaseActivity() {
this.mCurrentPhotoPath = null this.mCurrentPhotoPath = null
this.multiplePictures = multiplePictures this.multiplePictures = multiplePictures
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 200) { granted -> // Test if we have the permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || granted) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
selectedChoice = SELECTED_CHOICE_TAKE_PICTURE
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) askForStoragePermission()
return
// Create the File where the photo should go }
try {
tempFile = ImageUtils.createImageFile(this)
mCurrentPhotoPath = "file:" + tempFile?.absolutePath
} catch (ex: IOException) {
// Error occurred while creating the File
}
// Continue only if the File was successfully created val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (tempFile != null) { // Ensure that there's a camera activity to handle the intent
Timber.d("tempFile: ${tempFile?.absolutePath}") if (takePictureIntent.resolveActivity(packageManager) != null) {
val photoURI = FileProvider.getUriForFile( // Create the File where the photo should go
this, try {
applicationContext.packageName + ".fileprovider", tempFile!! tempFile = ImageUtils.createImageFile(this)
) mCurrentPhotoPath = "file:" + tempFile?.absolutePath
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) } catch (ex: IOException) {
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE) // Error occurred while creating the File
} }
// Continue only if the File was successfully created
if (tempFile != null) {
Timber.d("tempFile: ${tempFile?.absolutePath}")
val photoURI = FileProvider.getUriForFile(
this,
applicationContext.packageName + ".fileprovider", tempFile!!
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE)
} }
} }
// // Test if we have the permission
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// selectedChoice = SELECTED_CHOICE_TAKE_PICTURE
// askForStoragePermission()
// return
// }
} }
@ -195,32 +188,20 @@ open class MediaActivity : BaseActivity() {
this.multiplePictures = multiplePictures this.multiplePictures = multiplePictures
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 201) { granted -> // Test if we have the permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || granted) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
val galleryIntent = Intent() selectedChoice = SELECTED_CHOICE_SELECT_PICTURE
galleryIntent.type = "image/*" askForStoragePermission()
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures) return
galleryIntent.action = Intent.ACTION_GET_CONTENT
startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
}
} }
this.multiplePictures = multiplePictures
// Test if we have the permission val galleryIntent = Intent()
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { galleryIntent.type = "image/*"
// selectedChoice = SELECTED_CHOICE_SELECT_PICTURE galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures)
// askForStoragePermission() galleryIntent.action = Intent.ACTION_GET_CONTENT
// return startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
// }
//
// this.multiplePictures = multiplePictures
//
// val galleryIntent = Intent()
// galleryIntent.type = "image/*"
// galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures)
// galleryIntent.action = Intent.ACTION_GET_CONTENT
// startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
} }
/** /**
@ -234,6 +215,43 @@ open class MediaActivity : BaseActivity() {
} }
} }
// /**
// * Ask for the acmera permission
// */
// private fun askForCameraPermission() {
// // Here, thisActivity is the current activity
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),
// PERMISSION_REQUEST_CAMERA)
// }
// }
// /**
// * Ask for camera and storage permission
// */
// private fun askForCameraAndStoragePermissions() {
//
// val permissions = ArrayList<String>()
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// permissions.add(Manifest.permission.CAMERA)
// }
//
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
// }
//
// if (permissions.size > 0) {
// ActivityCompat.requestPermissions(this, permissions.toArray(arrayOfNulls<String>(permissions.size)), PERMISSION_REQUEST_CAMERA)
// }
// }
//
// /**
// * Called when a bitmap is return
// *
// * @param bitmap the bitmap returned
// */
// open fun getBitmapImage(file: File?, bitmap: Bitmap?) {}
/** /**
* Called when the user is adding new photos * Called when the user is adding new photos

@ -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
@ -20,23 +17,23 @@ import java.lang.ref.WeakReference
* Home Adapter * Home Adapter
*/ */
class HomePagerAdapter(fragmentManager: FragmentManager) : class HomePagerAdapter(fragmentManager: FragmentManager) :
FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private var weakReferences = SparseArray<WeakReference<BaseFragment>>() private var weakReferences = SparseArray<WeakReference<BaseFragment>>()
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()
} }
} }

@ -1,5 +1,6 @@
package net.pokeranalytics.android.ui.adapter package net.pokeranalytics.android.ui.adapter
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -64,8 +65,4 @@ class RowRepresentableAdapter(
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
fun changeDataSource(dataSource: RowRepresentableDataSource) {
this.dataSource = dataSource
}
} }

@ -9,11 +9,13 @@ 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
@ -34,257 +36,177 @@ import java.io.File
// Sizes // Sizes
val Int.dp: Int val Int.dp: Int
get() = (this / Resources.getSystem().displayMetrics.density).toInt() get() = (this / Resources.getSystem().displayMetrics.density).toInt()
val Int.px: Int val Int.px: Int
get() = (this * Resources.getSystem().displayMetrics.density).toInt() get() = (this * Resources.getSystem().displayMetrics.density).toInt()
val Float.dp: Float val Float.dp: Float
get() = (this / Resources.getSystem().displayMetrics.density) get() = (this / Resources.getSystem().displayMetrics.density)
val Float.px: Float val Float.px: Float
get() = (this * Resources.getSystem().displayMetrics.density) get() = (this * Resources.getSystem().displayMetrics.density)
// Toast // Toast
fun Activity.toast(message: String) { fun Activity.toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
fun Fragment.toast(message: String) { fun Fragment.toast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
} }
// Open Play Store for rating // Open Play Store for rating
fun Activity.openPlayStorePage() { fun Activity.openPlayStorePage() {
val uri = Uri.parse("market://details?id=$packageName") val uri = Uri.parse("market://details?id=$packageName")
val goToMarket = Intent(Intent.ACTION_VIEW, uri) val goToMarket = Intent(Intent.ACTION_VIEW, uri)
goToMarket.addFlags( goToMarket.addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK Intent.FLAG_ACTIVITY_MULTIPLE_TASK
) )
try { try {
startActivity(goToMarket) startActivity(goToMarket)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
startActivity( startActivity(
Intent( Intent(
Intent.ACTION_VIEW, Uri.parse( Intent.ACTION_VIEW, Uri.parse(
"http://play.google.com/store/apps/details?id=$packageName" "http://play.google.com/store/apps/details?id=$packageName"
) )
) )
) )
} }
} }
// Open email for "Contact us" // Open email for "Contact us"
fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null) { fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null) {
val info = val info =
"v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}"
val emailIntent = Intent(Intent.ACTION_SEND) val emailIntent = Intent(Intent.ACTION_SEND)
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, if (contentUri != null) {
"net.pokeranalytics.android.fileprovider", emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
databaseFile emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri))
) emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
if (contentUri != null) { }
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }
emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri))
emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
}
}
emailIntent.type = "message/rfc822" emailIntent.type = "message/rfc822"
emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes)) emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes))
emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info") emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info")
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value)) emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value))
startActivity(Intent.createChooser(emailIntent, getString(R.string.contact))) startActivity(Intent.createChooser(emailIntent, getString(R.string.contact)))
} }
// Open custom tab // Open custom tab
fun Context.openUrl(url: String) { fun Context.openUrl(url: String) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
ContextCompat.startActivity(this, browserIntent, null) ContextCompat.startActivity(this, browserIntent, null)
}
// Open custom tab
fun Context.areYouSure(
title: Int? = null,
message: Int? = null,
positiveTitle: Int? = null,
proceed: () -> Unit
) {
val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this)
val messageResource = message ?: R.string.are_you_sure_you_want_to_do_this
builder.setMessage(messageResource)
title?.let { builder.setTitle(it) }
val positiveButtonTitle = positiveTitle ?: R.string.yes
builder.setPositiveButton(positiveButtonTitle) { _, _ ->
proceed()
}
builder.setNegativeButton(R.string.cancel) { _, _ ->
// nothing
}
builder.create().show()
} }
// Display Alert Dialog // Display Alert Dialog
fun Activity.showAlertDialog(title: Int? = null, message: Int? = null) { 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, context?.let {
messageResId: Int? = null, showAlertDialog(it, title, message)
message: String? = null }
) {
context?.let {
showAlertDialog(it, title, messageResId, message)
}
}
fun showEditTextAlertDialog(
context: Context, inputType: Int, title: Int? = null, messageResId: Int? = null, message: String? = null,
editTextText: String? = null, positiveAction: ((String) -> Unit)? = null
) {
val builder = AlertDialog.Builder(context)
title?.let {
builder.setTitle(title)
}
messageResId?.let {
builder.setMessage(messageResId)
}
message?.let {
builder.setMessage(it)
}
val 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 { message?.let {
builder.setMessage(messageResId) builder.setMessage(message)
} }
message?.let { builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
builder.setMessage(it) positiveAction?.invoke()
} }
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> if (cancelButtonTitle != null) {
positiveAction?.invoke() builder.setNegativeButton(cancelButtonTitle) { _, _ ->
} negativeAction?.invoke()
}
if (cancelButtonTitle != null) { } else if (showCancelButton) {
builder.setNegativeButton(cancelButtonTitle) { _, _ -> builder.setNegativeButton(R.string.cancel) { _, _ ->
negativeAction?.invoke() negativeAction?.invoke()
} }
} else if (showCancelButton) { }
builder.setNegativeButton(R.string.cancel) { _, _ -> builder.show()
negativeAction?.invoke()
}
}
builder.show()
} }
fun TextView.setTextFormat(textFormat: TextFormat, context: Context) { fun TextView.setTextFormat(textFormat: TextFormat, context: Context) {
this.setTextColor(textFormat.getColor(context)) this.setTextColor(textFormat.getColor(context))
this.text = textFormat.text this.text = textFormat.text
} }
fun View.hideWithAnimation() { fun View.hideWithAnimation() {
isVisible = true isVisible = true
animate().cancel() animate().cancel()
animate().alpha(0f).withEndAction { isVisible = false }.start() animate().alpha(0f).withEndAction { isVisible = false }.start()
} }
fun View.showWithAnimation() { fun View.showWithAnimation() {
isVisible = true isVisible = true
animate().cancel() animate().cancel()
animate().alpha(1f).start() animate().alpha(1f).start()
} }
fun View.addCircleRipple() = with(TypedValue()) { fun View.addCircleRipple() = with(TypedValue()) {
context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true) context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true)
setBackgroundResource(resourceId) setBackgroundResource(resourceId)
} }
fun SearchView.removeMargins() { fun SearchView.removeMargins() {
val searchEditFrame = findViewById<LinearLayout?>(R.id.search_edit_frame) val searchEditFrame = findViewById<LinearLayout?>(R.id.search_edit_frame)
val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams? val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams?
layoutParams?.leftMargin = 0 layoutParams?.leftMargin = 0
layoutParams?.rightMargin = 0 layoutParams?.rightMargin = 0
searchEditFrame?.layoutParams = layoutParams searchEditFrame?.layoutParams = layoutParams
} }
fun View.toByteArray(): ByteArray { fun View.toByteArray() : ByteArray {
return this.convertToBitmap().toByteArray() return this.convertToBitmap().toByteArray()
} }
fun View.convertToBitmap(): Bitmap { fun View.convertToBitmap(): Bitmap {
val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(b) val canvas = Canvas(b)
val background = this.background val background = this.background
this.background?.let { this.background?.let {
background.draw(canvas) background.draw(canvas)
} ?: run { } ?: run {
canvas.drawColor(Color.WHITE) canvas.drawColor(Color.WHITE)
} }
this.draw(canvas) this.draw(canvas)
return b return b
} }
fun ImageView.toByteArray(): ByteArray { fun ImageView.toByteArray() : ByteArray {
return this.drawable.toBitmap().toByteArray() return this.drawable.toBitmap().toByteArray()
} }
fun Bitmap.toByteArray(): ByteArray { fun Bitmap.toByteArray() : ByteArray {
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
this.compress(Bitmap.CompressFormat.PNG, 100, baos) this.compress(Bitmap.CompressFormat.PNG, 100, baos)
return baos.toByteArray() return baos.toByteArray()
} }
fun Context.hideKeyboard(v: View) { fun Context.hideKeyboard(v: View) {
val imm = v.context.getSystemService(InputMethodManager::class.java) val imm = v.context.getSystemService(InputMethodManager::class.java)
imm?.hideSoftInputFromWindow(v.windowToken, 0) imm?.hideSoftInputFromWindow(v.windowToken, 0)
} }
//fun Context.showKeyboard(view: View) { //fun Context.showKeyboard(view: View) {

@ -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) Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
} catch (e: Exception) { }.isNotEmpty()
null }.sortedBy {
} it.displayName
}.toSet() }.map {
.filter { !mostUsedCurrencyCodes.contains(it.currencyCode) } CurrencyRow(it)
.filter { }
UserDefaults.availableCurrencyLocales.any { currencyLocale ->
Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
}
}
.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,17 +21,17 @@ 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
private val binding get() = _binding!! private val binding get() = _binding!!
private val exceptions: MutableList<Exception> = mutableListOf() // Life Cycle
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
@ -47,6 +44,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
_binding = null _binding = null
} }
fun setData(path: String) { fun setData(path: String) {
this.filePath = path this.filePath = path
} }
@ -55,10 +53,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,33 +85,34 @@ 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 { var exception: Exception? = null
GlobalScope.launch(coroutineContext) {
val coroutine = GlobalScope.async { val test = GlobalScope.async {
val s = Date() val s = Date()
Timber.d(">>> Start Import...") Timber.d(">>> Start Import...")
try { try {
importer.start() importer.start()
} catch (e: ImportException) { } catch (e: Exception) {
exceptions.add(e) exception = 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 = exception?.message
if (exceptionMessage != null && view != null) { if (exceptionMessage != null && view != null) {
val message = "${exceptions.size} " + requireContext().getString(R.string.errors) + ":\n" + exceptionMessage + ".\n" + requireContext().getString(R.string.import_error) val message = exceptionMessage + ". " + requireContext().getString(R.string.import_error)
val snackBar = Snackbar.make(view!!, message, Snackbar.LENGTH_INDEFINITE) val snackBar = Snackbar.make(view!!, message, Snackbar.LENGTH_INDEFINITE)
snackBar.setAction(R.string.ok) { snackBar.setAction(R.string.ok) {
snackBar.dismiss() snackBar.dismiss()
@ -127,6 +122,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 +142,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()
} }
@ -155,8 +160,4 @@ class ImportFragment : RealmFragment(), ImportDelegate {
binding.totalCounter.text = this.numberFormatter.format(totalCount) binding.totalCounter.text = this.numberFormatter.format(totalCount)
} }
override fun exceptionCaught(e: Exception) {
this.exceptions.add(e)
}
} }

@ -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()
}
} }
/** /**
@ -188,116 +140,35 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
adapter = dataListAdapter adapter = dataListAdapter
} }
binding.addButton.setOnClickListener { binding.addButton.setOnClickListener {
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,36 @@ 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.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 +44,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 +61,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 +104,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,7 +111,19 @@ 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) Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(Currency::class.java).isNull("code").or().equalTo("code", UserDefaults.currency.currencyCode).findAll().forEach { currency ->
currency.rate = Currency.DEFAULT_RATE
}
realm.where(Session::class.java).isNull("bankroll.currency.code").findAll().forEach { session ->
session.bankrollHasBeenUpdated()
}
}
realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
} }
} }
} }
@ -163,52 +133,8 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
} }
} }
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())
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(Currency::class.java).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate
}
realm.where(Session::class.java).findAll().forEach { session ->
session.bankrollHasBeenUpdated()
}
}
realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
}
override fun adapterRows(): List<RowRepresentable> { override fun adapterRows(): List<RowRepresentable> {
return SettingsRow.getRows() return rowRepresentation
} }
override fun charSequenceForRow( override fun charSequenceForRow(
@ -220,7 +146,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 -> ""
} }
} }
@ -228,9 +153,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
override fun boolForRow(row: RowRepresentable): Boolean { override fun boolForRow(row: RowRepresentable): Boolean {
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.SHOW_INAPP_BADGES -> Preferences.showInAppBadges(requireContext())
SettingsRow.BACKUP_EMAIL -> !Preferences.hasBackupEmail(requireContext())
else -> false else -> false
} }
} }
@ -247,13 +169,10 @@ 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.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 -> {
@ -264,7 +183,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
3 -> parentActivity?.openUrl(URL.FACEBOOK.value) 3 -> parentActivity?.openUrl(URL.FACEBOOK.value)
} }
} }
SettingsRow.BLOG_TIPS -> parentActivity?.openUrl(URL.BLOG_TIPS.value)
SettingsRow.POKER_RUMBLE -> parentActivity?.openUrl(URL.POKER_RUMBLE.value) SettingsRow.POKER_RUMBLE -> parentActivity?.openUrl(URL.POKER_RUMBLE.value)
SettingsRow.DISCORD -> parentActivity?.openUrl(URL.DISCORD.value) SettingsRow.DISCORD -> parentActivity?.openUrl(URL.DISCORD.value)
SettingsRow.PRIVACY_POLICY -> parentActivity?.openUrl(URL.PRIVACY_POLICY.value) SettingsRow.PRIVACY_POLICY -> parentActivity?.openUrl(URL.PRIVACY_POLICY.value)
@ -277,40 +195,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 +208,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.
} }
} }
@ -341,18 +223,36 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
} }
Preferences.setShowStopNotifications(show, requireContext()) Preferences.setShowStopNotifications(show, requireContext())
} }
SettingsRow.SHOULD_SHOW_BLOG_TIPS -> {
val show = value as Boolean
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 +284,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.pokerAnalyticsAndroid.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,14 @@ 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.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 +34,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
private lateinit var tableReportFragment: ComposableTableReportFragment private lateinit var tableReportFragment: ComposableTableReportFragment
private var transactionFilterMenuItem: MenuItem? = null
companion object { companion object {
/** /**
@ -72,10 +68,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
initUI() initUI()
this.currentFilterable = FilterableType.SESSION this.currentFilterable = FilterableType.SESSION
applyFilter() applyFilter()
listenRealmChanges(this, ComputableResult::class.java)
addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java)
} }
private fun initUI() { private fun initUI() {
@ -86,41 +79,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 +126,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 +145,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 +161,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 +177,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(
@ -203,14 +187,13 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
Stat.AVERAGE, Stat.AVERAGE,
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_SETS,
Stat.AVERAGE_HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION,
Stat.HOURLY_DURATION, Stat.HOURLY_DURATION
Stat.HANDS_PLAYED
) )
val query = filter?.query ?: Query() val query = filter?.query ?: Query()
val allSessionGroup = ComputableGroup(query, allStats) val allSessionGroup = ComputableGroup(query, allStats)
val cgStats: MutableList<Stat> = mutableListOf( val cgStats: List<Stat> = listOf(
Stat.NET_RESULT, Stat.NET_RESULT,
Stat.HOURLY_RATE, Stat.HOURLY_RATE,
Stat.NET_BB_PER_100_HANDS, Stat.NET_BB_PER_100_HANDS,
@ -221,27 +204,19 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_GAMES,
Stat.AVERAGE_BUYIN Stat.AVERAGE_BUYIN
) )
if (QueryCondition.IsCash.queryWith(realm.where(Result::class.java).isNotNull("tips")).count() > 0) {
cgStats.add(Stat.TOTAL_TIPS)
}
val cgSessionGroup = ComputableGroup(Query(QueryCondition.IsCash).merge(query), cgStats) val cgSessionGroup = ComputableGroup(Query(QueryCondition.IsCash).merge(query), cgStats)
val tStats: MutableList<Stat> = mutableListOf( val tStats: List<Stat> =
listOf(
Stat.NET_RESULT, Stat.NET_RESULT,
Stat.HOURLY_RATE, Stat.HOURLY_RATE,
Stat.ROI, Stat.ROI,
Stat.TOURNAMENT_ITM_RATIO, Stat.WIN_RATIO,
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_GAMES,
Stat.AVERAGE_BUYIN Stat.AVERAGE_BUYIN
) )
if (QueryCondition.IsTournament.queryWith(realm.where(Result::class.java).isNotNull("tips")).count() > 0) {
tStats.add(Stat.TOTAL_TIPS)
}
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 +225,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)
} }

@ -1,6 +1,5 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.content.res.Resources
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -30,13 +29,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 +49,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 +72,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
} }
@ -122,21 +118,14 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
if (indexOfLastSpace >= 0) { if (indexOfLastSpace >= 0) {
try { // can crash for unknown reasons val end = upgradeString.chars().count().toInt()
val lightTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_light)
val end = upgradeString.chars().count().toInt() val boldTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_bold)
val lightTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_light)
val boldTypeFace = ResourcesCompat.getFont(requireContext(), R.font.roboto_bold)
if (lightTypeFace != null && boldTypeFace != null) {
ssb.setSpan(TypefaceSpan(lightTypeFace), 0, indexOfLastSpace, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
ssb.setSpan(TypefaceSpan(boldTypeFace), indexOfLastSpace, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
}
} catch (e: Resources.NotFoundException) { if (lightTypeFace != null && boldTypeFace != null) {
CrashLogging.log(e.message ?: "ResourcesCompat.getFont crashes somehow") ssb.setSpan(TypefaceSpan(lightTypeFace), 0, indexOfLastSpace, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
ssb.setSpan(TypefaceSpan(boldTypeFace), indexOfLastSpace, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
} }
} }
title.text = ssb title.text = ssb
@ -159,22 +148,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 +217,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 {
val perYearString = requireContext().getString(R.string.year_subscription)
var price: String? = null val formattedPrice = it.price + " / " + perYearString
var freeTrialPeriod: String? = null binding.price.text = formattedPrice
productDetails.subscriptionOfferDetails?.firstOrNull()?.let { details -> var freeTrialDays = 30 // initial, should be more, no less
details.pricingPhases.pricingPhaseList.forEach { pricingPhase -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
when (pricingPhase.priceAmountMicros) { val p = Period.parse(it.freeTrialPeriod)
0L -> { freeTrialDays = p.days
freeTrialPeriod = pricingPhase.billingPeriod } catch (e: DateTimeParseException) {
} CrashLogging.log("Error parsing period with value: ${it.freeTrialPeriod}")
else -> {
price = pricingPhase.formattedPrice
}
}
}
}
price?.let {
val perYearString = requireContext().getString(R.string.year_subscription)
val formattedPrice = "$it / $perYearString"
binding.price.text = formattedPrice
}
freeTrialPeriod?.let {
var freeTrialDays = 30 // initial, should be more, no less
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
val p = Period.parse(it)
freeTrialDays = p.days
} catch (e: DateTimeParseException) {
CrashLogging.log("Error parsing period with value: $it")
}
} }
val formattedFreeTrial =
"$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial)
binding.freetrial.text = formattedFreeTrial
} }
val formattedFreeTrial =
"$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial)
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 +253,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 +278,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,22 @@
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 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 +40,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 +62,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 +161,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)
} }
@ -134,7 +133,9 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
viewGroup, viewGroup,
false false
) )
layoutCurrentFilter.findViewById<TextView>(R.id.filterName)?.text = filter.getDisplayName(requireContext()) layoutCurrentFilter.findViewById<TextView>(R.id.filterName)?.text = filter.getDisplayName(
requireContext()
)
layoutCurrentFilter.findViewById<ImageView>(R.id.deselectFilter).setOnClickListener { layoutCurrentFilter.findViewById<ImageView>(R.id.deselectFilter).setOnClickListener {
saveFilter(requireContext(), "") saveFilter(requireContext(), "")
} }

@ -47,7 +47,7 @@ class LoaderDialogFragment: DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
arguments?.let { bundle -> arguments?.let {bundle ->
if (bundle.containsKey(ARGUMENT_MESSAGE_RES_ID)) { if (bundle.containsKey(ARGUMENT_MESSAGE_RES_ID)) {
binding.loadingMessage.text = getString(bundle.getInt(ARGUMENT_MESSAGE_RES_ID)) binding.loadingMessage.text = getString(bundle.getInt(ARGUMENT_MESSAGE_RES_ID))
} }

@ -4,13 +4,10 @@ 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
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -56,26 +53,16 @@ open class RealmFragment : BaseFragment() {
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
/** fun listenRealmChanges(listener: RealmAsyncListener, clazz: Class<out RealmModel>) {
* Get the realm instance
*/
fun getRealm(): Realm {
return this.realm
}
fun addRealmChangeListener(listener: RealmAsyncListener, clazz: Class<out RealmModel>) {
if (this.changeListener != null && this.changeListener != listener) {
throw PAIllegalStateException("You cannot use a different listener")
}
this.changeListener = listener this.changeListener = listener
val results = this.realm.where(clazz).findAllAsync()
results.addChangeListener { t, _ -> this.realmResults = this.realm.where(clazz).findAllAsync()
this.realmResults?.addChangeListener { t, _ ->
Timber.d("Realm changes: ${realmResults?.size}, $this") Timber.d("Realm changes: ${realmResults?.size}, $this")
this.changeListener?.asyncListenedEntityChange(t.realm) this.changeListener?.asyncListenedEntityChange(t.realm)
} }
this.observedRealmResults.add(results)
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -89,6 +76,13 @@ open class RealmFragment : BaseFragment() {
this.realmResults?.removeAllChangeListeners() this.realmResults?.removeAllChangeListeners()
} }
/**
* Get the realm instance
*/
fun getRealm(): Realm {
return this.realm
}
/** /**
* A list of RealmModel classes to observe * A list of RealmModel classes to observe
*/ */

@ -12,12 +12,12 @@ import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorExcep
import net.pokeranalytics.android.util.extensions.round import net.pokeranalytics.android.util.extensions.round
open class BottomSheetDoubleEditTextFragment : BottomSheetFragment() { class BottomSheetDoubleEditTextFragment : BottomSheetFragment() {
private var _binding: BottomSheetDoubleEditTextBinding? = null private var _binding: BottomSheetDoubleEditTextBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetDoubleEditTextBinding.inflate(inflater, container, true) _binding = BottomSheetDoubleEditTextBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -16,7 +16,7 @@ class BottomSheetEditTextFragment : BottomSheetFragment() {
private var _binding: BottomSheetEditTextBinding? = null private var _binding: BottomSheetEditTextBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetEditTextBinding.inflate(inflater, container, true) _binding = BottomSheetEditTextBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -15,7 +15,7 @@ class BottomSheetEditTextMultiLinesFragment : BottomSheetFragment() {
private var _binding: BottomSheetEditTextMultiLinesBinding? = null private var _binding: BottomSheetEditTextMultiLinesBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetEditTextMultiLinesBinding.inflate(inflater, container, true) _binding = BottomSheetEditTextMultiLinesBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }
@ -32,12 +32,7 @@ class BottomSheetEditTextMultiLinesFragment : BottomSheetFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
binding.editText.requestFocus()
val data = getDescriptors() ?: throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found")
if (data[0].defaultValue.toString().isEmpty()) {
binding.editText.requestFocus()
}
} }
/** /**

@ -86,7 +86,6 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
BottomSheetType.DOUBLE_EDIT_TEXT -> BottomSheetDoubleEditTextFragment() BottomSheetType.DOUBLE_EDIT_TEXT -> BottomSheetDoubleEditTextFragment()
BottomSheetType.NUMERIC_TEXT -> BottomSheetNumericTextFragment() BottomSheetType.NUMERIC_TEXT -> BottomSheetNumericTextFragment()
BottomSheetType.SUM -> BottomSheetSumFragment() BottomSheetType.SUM -> BottomSheetSumFragment()
BottomSheetType.CASH_GAME_STAKES -> BottomSheetStakesFragment()
} }
} }
@ -112,10 +111,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 +215,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

@ -18,7 +18,7 @@ open class BottomSheetListFragment : BottomSheetFragment(), LiveRowRepresentable
private var _binding: BottomSheetListBinding? = null private var _binding: BottomSheetListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetListBinding.inflate(inflater, container, true) _binding = BottomSheetListBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -22,7 +22,7 @@ class BottomSheetListGameFragment : BottomSheetListFragment() {
private var _binding: BottomSheetGameListBinding? = null private var _binding: BottomSheetGameListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetGameListBinding.inflate(inflater, container, true) _binding = BottomSheetGameListBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -17,7 +17,7 @@ class BottomSheetNumericTextFragment : BottomSheetFragment() {
private var _binding: BottomSheetEditTextBinding? = null private var _binding: BottomSheetEditTextBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetEditTextBinding.inflate(inflater, container, true) _binding = BottomSheetEditTextBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -1,151 +0,0 @@
package net.pokeranalytics.android.ui.fragment.components.bottomsheet
import android.content.Context
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.widget.addTextChangedListener
import net.pokeranalytics.android.databinding.BottomSheetStakesBinding
import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException
import java.text.NumberFormat
import java.text.ParseException
class BottomSheetStakesFragment : BottomSheetFragment() {
private var _binding: BottomSheetStakesBinding? = null
private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View {
_binding = BottomSheetStakesBinding.inflate(inflater, container, true)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
override fun onStart() {
super.onStart()
}
/**
* Init data
*/
private fun initData() {
// this.viewModel.isEditingBlinds = this.viewModel.row == SessionRow.BLINDS
}
private fun focusEditTextAndHideKeyboard(editText: EditText) {
editText.requestFocus()
editText.onCreateInputConnection(EditorInfo())?.let {
binding.stakesKeyboard.inputConnection = it
}
val mgr: InputMethodManager? =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
mgr?.hideSoftInputFromWindow(editText.windowToken, InputMethodManager.SHOW_FORCED)
}
/**
* Init UI
*/
private fun initUI() {
val data = getDescriptors()?:throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor not found")
if (data.size != 2) {
throw RowRepresentableEditDescriptorException("RowRepresentableEditDescriptor inconsistency")
}
// Ante
val anteED = data[0]
anteED.defaultValue?.let {
binding.anteEditText.hint = NumberFormat.getInstance().format(it as Double)
} ?: run {
anteED.hintResId?.let { binding.anteEditText.hint = getString(it) }
}
// binding.anteEditText.inputType = anteED.inputType ?: InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
// binding.anteKeyboard.toolbar.isVisible = false
// binding.anteKeyboard.visibility = View.GONE
binding.anteEditText.setRawInputType(InputType.TYPE_CLASS_NUMBER)
binding.anteEditText.showSoftInputOnFocus = false
// Blinds
val blindsED = data[1]
blindsED.defaultValue?.let {
binding.blindsEditText.hint = it as? String
} ?: run {
blindsED.hintResId?.let { binding.blindsEditText.hint = getString(it) }
}
binding.blindsEditText.onCreateInputConnection(EditorInfo())?.let {
binding.stakesKeyboard.inputConnection = it
}
binding.blindsEditText.setRawInputType(InputType.TYPE_CLASS_NUMBER)
binding.blindsEditText.showSoftInputOnFocus = false
binding.blindsEditText.setOnTouchListener { _, _ ->
this.focusEditTextAndHideKeyboard(binding.blindsEditText)
// binding.stakesKeyboard.visibility = View.VISIBLE
binding.stakesKeyboard.setSeparatorVisibility(true)
return@setOnTouchListener true
}
binding.anteEditText.setOnTouchListener { _, _ ->
// binding.anteKeyboard.setInputConnection(binding.anteEditText)
this.focusEditTextAndHideKeyboard(binding.anteEditText)
binding.stakesKeyboard.setSeparatorVisibility(false)
// binding.stakesKeyboard.value_separator.visibility = View.GONE
// binding.stakesKeyboard.visibility = View.VISIBLE
// binding.stakesKeyboard.visibility = View.GONE
return@setOnTouchListener true
}
binding.anteEditText.addTextChangedListener { text ->
text?.let {
val ante = try {
NumberFormat.getInstance().parse(it.toString())
} catch(e: ParseException) {
null
}
this.model.ante = ante?.toDouble()
}
}
binding.blindsEditText.addTextChangedListener {
this.model.secondStringValue = it?.toString()
}
}
}

@ -17,7 +17,7 @@ class BottomSheetStaticListFragment : BottomSheetFragment(), StaticRowRepresenta
private var _binding: BottomSheetListBinding? = null private var _binding: BottomSheetListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetListBinding.inflate(inflater, container, true) _binding = BottomSheetListBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -19,7 +19,7 @@ class BottomSheetSumFragment : BottomSheetFragment() {
private var _binding: BottomSheetSumBinding? = null private var _binding: BottomSheetSumBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetSumBinding.inflate(inflater, container, true) _binding = BottomSheetSumBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }

@ -19,7 +19,7 @@ class BottomSheetTableSizeGridFragment : BottomSheetFragment(), StaticRowReprese
private var _binding: BottomSheetGridBinding? = null private var _binding: BottomSheetGridBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateContentView(inflater: LayoutInflater, container: ViewGroup): View? {
_binding = BottomSheetGridBinding.inflate(inflater, container, true) _binding = BottomSheetGridBinding.inflate(inflater, container, true)
return binding.root return binding.root
} }
@ -61,7 +61,7 @@ class BottomSheetTableSizeGridFragment : BottomSheetFragment(), StaticRowReprese
} }
} }
override fun adapterRows(): List<RowRepresentable> { override fun adapterRows(): List<RowRepresentable>? {
return TableSize.all(this.model.alternativeLabels) return TableSize.all(this.model.alternativeLabels)
} }

@ -12,8 +12,7 @@ enum class BottomSheetType {
EDIT_TEXT_MULTI_LINES, EDIT_TEXT_MULTI_LINES,
DOUBLE_EDIT_TEXT, DOUBLE_EDIT_TEXT,
NUMERIC_TEXT, NUMERIC_TEXT,
SUM, SUM;
CASH_GAME_STAKES;
val validationRequired: Boolean val validationRequired: Boolean
get() = when (this) { get() = when (this) {
@ -26,7 +25,7 @@ enum class BottomSheetType {
val addRequired: Boolean val addRequired: Boolean
get() = when (this) { get() = when (this) {
EDIT_TEXT, NUMERIC_TEXT, DOUBLE_EDIT_TEXT, EDIT_TEXT_MULTI_LINES, GRID, LIST_STATIC, SUM, CASH_GAME_STAKES -> false EDIT_TEXT, NUMERIC_TEXT, DOUBLE_EDIT_TEXT, EDIT_TEXT_MULTI_LINES, GRID, LIST_STATIC, SUM -> false
else -> true else -> true
} }
} }

@ -3,10 +3,10 @@ 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 androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import net.pokeranalytics.android.R import androidx.core.content.ContextCompat
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.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
@ -58,17 +58,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,19 +79,28 @@ 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 { saveReport(nameEditText.text.toString())
saveReport(nameEditText.text.toString()) dialog.dismiss()
dialog.dismiss()
} catch (e: Exception) {
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")
@ -100,10 +109,6 @@ abstract class AbstractReportFragment : DataManagerFragment() {
private fun saveReport(name: String) { private fun saveReport(name: String) {
if (name.isEmpty()) {
throw PAIllegalStateException(getString(R.string.report_name_cannot_be_empty))
}
this.reportViewModel.title = name this.reportViewModel.title = name
val rs = this.model.item as ReportSetup val rs = this.model.item as ReportSetup
getRealm().executeTransaction { realm -> getRealm().executeTransaction { realm ->

@ -7,7 +7,6 @@ import android.view.ViewGroup
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import net.pokeranalytics.android.databinding.FragmentReportDetailsBinding import net.pokeranalytics.android.databinding.FragmentReportDetailsBinding
import net.pokeranalytics.android.ui.adapter.ReportPagerAdapter import net.pokeranalytics.android.ui.adapter.ReportPagerAdapter
import net.pokeranalytics.android.ui.helpers.AppReviewManager
class ComparisonReportFragment : AbstractReportFragment() { class ComparisonReportFragment : AbstractReportFragment() {
@ -28,8 +27,7 @@ class ComparisonReportFragment : AbstractReportFragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
AppReviewManager.requestReview() _binding = null
_binding = null
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {

@ -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(requireContext()).capitalize()
val title = result.group.query.getName(context).capitalize() rows.add(CustomizableRowRepresentable(title = title))
rows.add(CustomizableRowRepresentable(title = title)) val statList = result.group.stats ?: 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(requireContext())))
rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(context)))
}
} }
} }
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
@ -185,6 +187,12 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
} }
this.hasComputationInProgress = true this.hasComputationInProgress = true
val cr = getRealm().where(ComputableResult::class.java).findAll()
if (cr.size < 2) {
Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show()
return
}
if (row is StatRow && row.stat.hasProgressGraph) { if (row is StatRow && row.stat.hasProgressGraph) {
// queryWith groups // queryWith groups
@ -193,13 +201,6 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
} }
groupResults?.firstOrNull()?.let { groupResults?.firstOrNull()?.let {
if (it.group.size < 2) {
Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show()
this.hasComputationInProgress = false
return
}
this.launchStatComputationWithEvolution(row.stat, it.group) this.launchStatComputationWithEvolution(row.stat, it.group)
} }
} }
@ -215,7 +216,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
@ -29,7 +29,6 @@ import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.extensions.showWithAnimation import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.helpers.AppReviewManager
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -82,7 +81,11 @@ class ProgressReportFragment : AbstractReportFragment() {
override fun onDestroyView() { override fun onDestroyView() {
AppReviewManager.requestReview() // childFragmentManager.findFragmentByTag(GRAPH_TAG)?.let { fragment ->
// val fragmentTransaction = childFragmentManager.beginTransaction()
// fragmentTransaction.remove(fragment)
// fragmentTransaction.commit()
// }
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
@ -166,27 +169,26 @@ 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)
reports[aggregationType] = report
realm.close() val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType)
reports[aggregationType] = report
val e = Date() realm.close()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) { val e = Date()
setGraphData(report, aggregationType) val duration = (e.time - s.time) / 1000.0
progressBar.hideWithAnimation() Timber.d(">>> ended in $duration seconds")
graphContainer.showWithAnimation()
}
}
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)
progressBar.hideWithAnimation()
graphContainer.showWithAnimation()
}
} }
} }

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

Loading…
Cancel
Save