Compare commits

..

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

  1. 1
      .gitignore
  2. 0
      .gitlab-ci.yml
  3. 139
      CLAUDE.md
  4. 125
      app/build.gradle
  5. 24
      app/google-services.json
  6. 25
      app/proguard-rules.pro
  7. 19
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/FavoriteSessionUnitTest.kt
  8. 8
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  9. 82
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt
  10. 39
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt
  11. 10
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt
  12. 80
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  13. 3
      app/src/debug/AndroidManifest.xml
  14. 134
      app/src/main/AndroidManifest.xml
  15. BIN
      app/src/main/ic_launcher-playstore.png
  16. 59
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  17. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  18. 49
      app/src/main/java/net/pokeranalytics/android/api/BlogPostApi.kt
  19. 60
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  20. 47
      app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt
  21. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  22. 10
      app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt
  23. 146
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  24. 111
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  25. 247
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  26. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  27. 42
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  28. 28
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  29. 10
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  30. 12
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/AggregationTypeExtensions.kt
  31. 84
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ComputedResultsExtensions.kt
  32. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportDisplay.kt
  33. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportExtensions.kt
  34. 81
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/StatRepresentable.kt
  35. 299
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  36. 25
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  37. 46
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  38. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  39. 13
      app/src/main/java/net/pokeranalytics/android/model/Stakes.kt
  40. 13
      app/src/main/java/net/pokeranalytics/android/model/blogpost/BlogPost.kt
  41. 27
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  42. 4
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  43. 51
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  44. 507
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  45. 80
      app/src/main/java/net/pokeranalytics/android/model/handhistory/BoardManager.kt
  46. 56
      app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt
  47. 2
      app/src/main/java/net/pokeranalytics/android/model/interfaces/CountableUsage.kt
  48. 14
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  49. 189
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  50. 168
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  51. 137
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  52. 85
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  53. 3
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  54. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  55. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Currency.kt
  56. 97
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  57. 3
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  58. 55
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  59. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  60. 15
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  61. 5
      app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt
  62. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  63. 138
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  64. 29
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  65. 18
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  66. 776
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  67. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  68. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  69. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt
  70. 94
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  71. 26
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  72. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  73. 56
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt
  74. 245
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  75. 14
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/BankrollRow.kt
  76. 57
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/CustomFieldRow.kt
  77. 11
      app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt
  78. 15
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  79. 31
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  80. 20
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  81. 48
      app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt
  82. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  83. 12
      app/src/main/java/net/pokeranalytics/android/ui/activity/GDPRActivity.kt
  84. 64
      app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt
  85. 175
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  86. 18
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  87. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt
  88. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt
  89. 117
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  90. 187
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt
  91. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt
  92. 128
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  93. 43
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt
  94. 11
      app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt
  95. 55
      app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt
  96. 7
      app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt
  97. 5
      app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt
  98. 302
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  99. 24
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt
  100. 81
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  101. Some files were not shown because too many files have changed in this diff Show More

1
.gitignore vendored

@ -6,7 +6,6 @@
# Built application files
*.apk
*.ap_
*.aab
/app/release
app/*2020/

@ -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,24 +1,21 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
// Crashlytics
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services' // Crashlytics
repositories {
maven { url 'https://maven.fabric.io/public' }
maven { url 'https://jitpack.io' } // required for MPAndroidChart
jcenter() // for kotlin serialization
}
android {
compileSdkVersion 35
buildToolsVersion "30.0.3"
compileSdkVersion 28
buildToolsVersion "28.0.3"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -29,33 +26,32 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
lintOptions {
disable 'MissingTranslation'
}
defaultConfig {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 35
versionCode 180
versionName "6.0.38"
targetSdkVersion 28
versionCode 93
versionName "5.0_alpha_1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
ext.enableCrashlytics = false
firebaseCrashlytics {
mappingFileUploadEnabled false // should help speed up build times: https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=en&platform=android
}
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationVariants.all { variant ->
variant.outputs.all { output ->
def date = new Date()
def formattedDate = date.format('yyMMdd_HHmm')
def appName = "PokerAnalytics"
def buildType = variant.buildType.name
def buildType = variant.variantData.variantConfiguration.buildType.name
def newName
if (buildType == 'debug'){
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk"
@ -68,15 +64,20 @@ android {
}
}
flavorDimensions 'endOfUse'
productFlavors { // already used: 50000, 51000, 52000, 52130, 52110, 52120
productFlavors {
standard {
dimension = 'endOfUse'
}
// oct2021 {
// dimension = 'endOfUse'
// versionNameSuffix = '_oct2021'
// versionCode = 52120 + android.defaultConfig.versionCode
// }
june2020 {
dimension = 'endOfUse'
versionNameSuffix = '_june2020'
versionCode = 52000 + android.defaultConfig.versionCode
}
august2020 {
dimension = 'endOfUse'
versionNameSuffix = '_august2020'
versionCode = 50000 + android.defaultConfig.versionCode
}
}
configurations {
@ -85,50 +86,39 @@ android {
}
}
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
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-json:1.4.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency
// Android
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
// Places
implementation 'com.google.android.libraries.places:places:2.3.0'
implementation 'com.google.android.libraries.places:places:1.1.0'
// Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0'
// WARNING FOR 2.0: https://developer.android.com/google/play/billing/billing_library_releases_notes
// Purchases MUST BE ACKNOWLEDGED
implementation 'com.android.billingclient:billing:1.2.2'
// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0')
// Declare the dependencies for the Crashlytics and Analytics libraries
// When using the BoM, you don't specify versions in Firebase library dependencies
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// Firebase
implementation 'com.google.firebase:firebase-core:17.4.2'
// Crashlytics
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
// Logs
implementation 'com.jakewharton.timber:timber:4.7.1'
@ -142,37 +132,14 @@ dependencies {
// Polynomial Regression
implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
// Image picking and registerForActivityResult
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation "androidx.fragment:fragment-ktx:1.4.1"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
// 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:rules:1.0.2'

@ -8,12 +8,20 @@
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:245968016816:android:e5597a41d79df0a31d7275",
"mobilesdk_app_id": "1:245968016816:android:47f8b4f74b1296b4",
"android_client_info": {
"package_name": "net.pokeranalytics.android"
}
},
"oauth_client": [
{
"client_id": "245968016816-tr2mo4kbe1acn8u3ebbd9nk29iuk8fqr.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "net.pokeranalytics.android",
"certificate_hash": "e26278fa6db56acde23b0ff5981692f7f60408b9"
}
},
{
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3
@ -25,20 +33,20 @@
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "245968016816-8sklai9sq70m46anv550uttdic6cukn6.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "stax.SlashPoker.nosebleed"
}
}
]
},
"ads_service": {
"status": 2
}
}
}

@ -30,9 +30,6 @@
-dontwarn io.realm.**
-keep class net.pokeranalytics.android.model.** { *; }
-keep class net.pokeranalytics.android.ui.fragment.** { *; }
-keep class net.pokeranalytics.android.ui.modules.** { *; }
-keepattributes SourceFile,LineNumberTable # Keep file names and line numbers.
-keep public class * extends java.lang.Exception # Optional: Keep custom exceptions.
# Retrofit
@ -65,25 +62,3 @@
# 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()
s3.endDate = Date()
s1.cgBlinds = "2/4"
s2.cgBlinds = "2/4"
s3.cgBlinds = "1/2"
s1.cgBigBlind = 4.0
s1.cgSmallBlind = 2.0
s2.cgBigBlind = 4.0
s2.cgSmallBlind = 2.0
s3.cgBigBlind = 1.0
s3.cgSmallBlind = 1.0
realm.insert(s1)
realm.insert(s2)
@ -42,7 +45,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) {
Assert.assertEquals(4.0, favSession.cgBiggestBet)
Assert.assertEquals(4.0, favSession.cgBigBlind)
} else {
Assert.fail("session shouldn't be null")
}
@ -62,9 +65,9 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val loc1 = realm.createObject(Location::class.java, "1")
val loc2 = realm.createObject(Location::class.java, "2")
s1.cgBiggestBet = 4.0
s2.cgBiggestBet = 4.0
s3.cgBiggestBet = 1.0
s1.cgBigBlind = 4.0
s2.cgBigBlind = 4.0
s3.cgBigBlind = 1.0
s1.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)
if (favSession != null) {
Assert.assertEquals(1.0, favSession.cgBiggestBet)
Assert.assertEquals(1.0, favSession.cgBigBlind)
} else {
Assert.fail("session shouldn't be null")
}

@ -44,8 +44,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300
s1.cgBlinds = "0.5" // bb net result = -200bb
s2.cgBlinds = "2.0" // bb net result = 150bb
s1.cgBigBlind = 0.5 // bb net result = -200bb
s2.cgBigBlind = 2.0 // bb net result = 150bb
s2.tableSize = 5
@ -186,13 +186,13 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
Assert.fail("No std100 stat")
}
results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let {
results.computedStat(Stat.MAXIMUM_NETRESULT)?.let {
assertEquals(300.0, it.value, delta)
} ?: run {
Assert.fail("No MAXIMUM_NETRESULT")
}
results.computedStat(Stat.MINIMUM_NET_RESULT)?.let {
results.computedStat(Stat.MINIMUM_NETRESULT)?.let {
assertEquals(-100.0, it.value, delta)
} ?: run {
Assert.fail("No MINIMUM_NETRESULT")

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import java.util.*
@ -28,25 +28,28 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
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)
s2.cgBlinds = "0.5/1"
s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2"
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyStake().apply {
val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
// blind.filterSectionRow = FilterSectionRow.Blind
blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -71,27 +74,33 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
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)
s2.cgBlinds = "0.5/1"
s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2"
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val filter = QueryCondition.AnyBlind()
val blind1 = QueryCondition.AnyStake().apply {
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
val blind2 = QueryCondition.AnyStake().apply {
val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!)
}
val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Stakes)
blind1.filterSectionRow = FilterSectionRow.Blind
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElements = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueBy(filterElements)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -116,24 +125,30 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
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)
s2.cgBlinds = "0.5/1"
s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2"
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyStake().apply {
val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s3.blinds!!)
}
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -157,27 +172,34 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
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)
s2.cgBlinds = "0.5/1"
s2.cgBigBlind = 2.0
s2.cgSmallBlind = 1.0
val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2"
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val stake1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.cgStakes!!)
val filter = QueryCondition.AnyBlind()
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
val stake2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.cgStakes!!)
val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!)
}
val filterElement = FilterCondition(arrayListOf(stake1, stake2), FilterSectionRow.Stakes)
blind1.filterSectionRow = FilterSectionRow.Blind
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.extensions.startOfDay
import org.junit.Assert
@ -35,7 +35,8 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -63,7 +64,8 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -90,7 +92,8 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyYear()
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -381,9 +384,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s2 = Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction()
val filter = QueryCondition.StartedFromDate(Date())
val filterElementRow = QueryCondition.StartedFromDate(Date()).apply { singleValue = s2.startDate!!}
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val filter = QueryCondition.StartedFromDate()
val filterElementRow = QueryCondition.StartedFromDate().apply { singleValue = s2.startDate!!}
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -408,9 +412,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.StartedToDate(Date())
val filterElementRow = QueryCondition.StartedToDate(Date()).apply { singleValue = s1.startDate!! }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val filter = QueryCondition.StartedToDate()
val filterElementRow = QueryCondition.StartedToDate().apply { singleValue = s1.startDate!! }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -436,9 +441,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.EndedFromDate(Date())
val filterElementRow = QueryCondition.EndedFromDate(Date()).apply { singleValue = s2.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val filter = QueryCondition.EndedFromDate()
val filterElementRow = QueryCondition.EndedFromDate().apply { singleValue = s2.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -464,9 +470,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.EndedToDate(Date())
val filterElementRow = QueryCondition.EndedToDate(Date()).apply { singleValue = s1.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val filter = QueryCondition.EndedToDate()
val filterElementRow = QueryCondition.EndedToDate().apply { singleValue = s1.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -5,9 +5,8 @@ import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.FixedValueFilterItemRow
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@ -25,9 +24,8 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
filter.name = "testSaveLoadCashFilter"
val filterElement = QueryCondition.IsCash
val filterItemRow = FixedValueFilterItemRow(filterElement, FilterSectionRow.CashOrTournament)
filter.createOrUpdateFilterConditions(arrayListOf(filterItemRow))
filterElement.filterSectionRow = FilterSectionRow.CashOrTournament
filter.createOrUpdateFilterConditions(arrayListOf(filterElement))
val useCount = filter.countBy(FilterCategoryRow.GENERAL)
Assert.assertEquals(1, useCount)

@ -6,7 +6,7 @@ import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@ -110,7 +110,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Bankroll))
filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -140,9 +141,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) }
filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Bankroll))
val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) }
filterElementRow2.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -166,7 +169,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val anyGame = QueryCondition.AnyGame(g2)
val fc = FilterCondition(arrayListOf(anyGame), FilterSectionRow.Game)
val fc = FilterCondition(filterElementRows = arrayListOf(anyGame))
val sessions = Filter.queryOn<Session>(realm, Query(fc.queryCondition))
Assert.assertEquals(1, sessions.size)
@ -194,10 +197,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) }
filterElementRow.filterSectionRow = FilterSectionRow.Game
val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) }
val filterCondition = FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Game)
filterElementRow2.filterSectionRow = FilterSectionRow.Game
val filterCondition = FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))
val queryCondition = filterCondition.queryCondition
val sessions = Filter.queryOn<Session>(realm, Query(queryCondition))
@ -222,7 +225,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Location))
filterElementRow.filterSectionRow = FilterSectionRow.Location
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -253,9 +257,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.Location
val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) }
filterElementRow2.filterSectionRow = FilterSectionRow.Location
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Location))
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -281,7 +287,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentName))
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -311,8 +318,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TournamentName))
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -345,10 +354,12 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AllTournamentFeature()
val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3), FilterSectionRow.TournamentFeature))
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -378,11 +389,14 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) }
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4), FilterSectionRow.TournamentFeature))
filterElementRow4.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -409,7 +423,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentFeature))
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -433,9 +448,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTableSize()
val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) }
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TableSize))
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -458,8 +474,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow))
@ -482,8 +499,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val filterElementRow = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -506,12 +524,14 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value))
val filterElementRow2 = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterElementRow2.filterSectionRow = FilterSectionRow.Value
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess))
@ -536,11 +556,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s3 = Session.testInstance(netResult = 500.0)
s3.cgBlinds = "2.0"
s3.cgBigBlind = 2.0
s3.result!!.buyin = 1000.0
val s4 = Session.testInstance(netResult = 570.0)
s4.cgBlinds = "5.0"
s4.cgBigBlind = 5.0
s4.result!!.buyin = 200.0
realm.commitTransaction()
@ -584,7 +604,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
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)

@ -1,5 +1,6 @@
<?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 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

@ -1,18 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
xmlns:tools="http://schemas.android.com/tools"
package="net.pokeranalytics.android">
<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="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".PokerAnalyticsApplication"
@ -26,14 +21,13 @@
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
android:value="false" />
<activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
@ -46,8 +40,7 @@
<activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
android:screenOrientation="portrait">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
@ -62,186 +55,121 @@
</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
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:windowSoftInputMode="adjustNothing" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
android:windowSoftInputMode="stateAlwaysHidden"/>
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
android:exported="true" />
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
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" />
android:screenOrientation="portrait" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog"
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" />
android:launchMode="singleTop"/>
<service
android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<service android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService" android:exported="false"/>
<meta-data
android:name="preloaded_fonts"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -3,19 +3,21 @@ package net.pokeranalytics.android
import android.app.Application
import android.content.Context
import android.os.Build
import com.google.firebase.FirebaseApp
import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import io.fabric.sdk.android.Fabric
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.Session
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 timber.log.Timber
import java.util.*
@ -23,9 +25,6 @@ import java.util.*
class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object {
fun timeSinceInstall(context: Context): Long {
@ -37,11 +36,6 @@ class PokerAnalyticsApplication : Application() {
override fun onCreate() {
super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
UserDefaults.init(this)
// AppGuard / Billing services
@ -51,23 +45,35 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(14)
.allowWritesOnUiThread(true)
.schemaVersion(9)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build()
Realm.setDefaultConfiguration(realmConfiguration)
// val realm = Realm.getDefaultInstance()
// realm.executeTransaction {
// realm.where(Session::class.java).findAll().deleteAllFromRealm()
// }
// realm.close()
// Set up Crashlytics, disabled for debug builds
val crashlyticsKit = Crashlytics.Builder()
.core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
.build()
// Initialize Fabric with the debug-disabled crashlytics.
Fabric.with(this, crashlyticsKit)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val locales = resources.configuration.locales
CrashLogging.log("App onCreate. Locales = $locales")
Crashlytics.log("App onCreate. Locales = $locales")
}
if (BuildConfig.DEBUG) {
// Logs
Timber.plant(PokerAnalyticsLogs())
}
Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
@ -76,23 +82,10 @@ class PokerAnalyticsApplication : Application() {
// this.createFakeSessions()
}
// Patch
Patcher.patchAll(this)
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
Patcher.patchAll(this.applicationContext)
// Infos
val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
Crashlytics.log("Country: ${locale.country}, language: ${locale.language}")
}
@ -106,7 +99,7 @@ class PokerAnalyticsApplication : Application() {
realm.close()
if (sessionsCount < 10) {
CoroutineScope(context = Dispatchers.IO).launch {
GlobalScope.launch {
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
}
}
}

@ -2,7 +2,7 @@ package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.graph.AxisFormatting
enum class AggregationType {
SESSION,
@ -20,6 +20,14 @@ enum class AggregationType {
}
}
val axisFormatting: AxisFormatting
get() {
return when (this) {
DURATION -> AxisFormatting.X_DURATION
else -> AxisFormatting.DEFAULT
}
}
val criterias: List<Criteria>
get() {
return when (this) {

@ -2,6 +2,7 @@ package net.pokeranalytics.android.calculus
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Criteria
@ -9,7 +10,13 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query
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.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.extensions.startOfDay
import java.util.*
import kotlin.math.max
@ -26,18 +33,18 @@ class Calculator {
* The options used for calculations and display
*/
class Options(
var progressValues: ProgressValues = ProgressValues.NONE,
var display: Display = Display.TABLE,
progressValues: ProgressValues = ProgressValues.NONE,
var stats: List<Stat> = listOf(),
var criterias: List<Criteria> = listOf(),
var query: Query = Query(),
var filterId: String? = null,
private var aggregationType: AggregationType? = null,
var userGenerated: Boolean = false,
var reportSetupId: String? = null,
var includedTransactions: List<TransactionType> = listOf()
var reportSetupId: String? = null
) {
constructor(
constructor(display: Display = Display.TABLE,
progressValues: ProgressValues = ProgressValues.NONE,
stats: List<Stat> = listOf(),
criterias: List<Criteria> = listOf(),
@ -45,18 +52,18 @@ class Calculator {
aggregationType: AggregationType? = null,
userGenerated: Boolean = false,
reportSetupId: String? = null) :
this(progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId)
this(display, progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId)
/**
* Specifies whether progress values should be added and their kind
*/
// var progressValues: ProgressValues = progressValues
// get() {
// if (field == ProgressValues.NONE && this.display.requireProgressValues) {
// return ProgressValues.STANDARD
// }
// return field
// }
var progressValues: ProgressValues = progressValues
get() {
if (field == ProgressValues.NONE && this.display == Display.PROGRESS) {
return ProgressValues.STANDARD
}
return field
}
init {
this.aggregationType?.let {
@ -64,6 +71,40 @@ class Calculator {
}
}
/**
* The way the computed stats are going to be displayed
*/
enum class Display : RowRepresentable {
TABLE,
PROGRESS,
COMPARISON,
MAP,
POLYNOMIAL;
override val resId: Int?
get() {
return when (this) {
TABLE -> R.string.table
PROGRESS -> R.string.progress
COMPARISON -> R.string.comparison
MAP -> R.string.map
POLYNOMIAL -> null
}
}
val activityClass: Class<*>
get() {
return when (this) {
TABLE -> TableReportActivity::class.java
PROGRESS -> ProgressReportActivity::class.java
COMPARISON -> ComparisonReportActivity::class.java
else -> throw PAIllegalStateException("undefined activity for report display")
// MAP -> R.string.map
// POLYNOMIAL -> null
}
}
}
/**
* The type of evolution numericValues
@ -94,7 +135,6 @@ class Calculator {
get() {
return this.stats.contains(LONGEST_STREAKS)
}
/**
* Whether the values should be sorted
*/
@ -102,7 +142,6 @@ class Calculator {
get() {
return this.progressValues != ProgressValues.NONE || this.computeLongestStreak
}
/**
* Whether the number of locations played should be computed
*/
@ -152,6 +191,7 @@ class Calculator {
): Report {
val options = Options(
display = Options.Display.PROGRESS,
progressValues = Options.ProgressValues.STANDARD,
stats = listOf(stat),
aggregationType = aggregationType
@ -177,13 +217,13 @@ class Calculator {
val computableGroups: MutableList<ComputableGroup> = mutableListOf()
val combinations = options.criterias.combined()
// Timber.d("Combinations: ${ combinations.map { it.defaultName }}")
options.criterias.combined().forEach { comparatorQuery ->
for (comparatorQuery in combinations) {
comparatorQuery.merge(options.query)
val group = ComputableGroup(comparatorQuery)
computableGroups.add(group)
}
if (computableGroups.size == 0) {
@ -201,6 +241,7 @@ class Calculator {
val report = Report(options)
groups.forEach { group ->
// val s = Date()
// Clean existing computables / sessionSets if group is reused
group.cleanup()
@ -240,27 +281,11 @@ class Calculator {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
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 ${computableGroup.name}, ${computables.size} computables")
results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())
// computables.forEach {
// Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}")
// }
var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
if (options.includedTransactions.isNotEmpty()) {
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 sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
results.addStat(NET_RESULT, sum)
val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
results.addStat(HANDS_PLAYED, totalHands)
@ -277,51 +302,37 @@ class Calculator {
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
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}")
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
maxNetResult?.let {
results.addStat(MAXIMUM_NET_RESULT, it)
results.addStat(MAXIMUM_NETRESULT, it)
}
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let {
results.addStat(MINIMUM_NET_RESULT, it)
results.addStat(MINIMUM_NETRESULT, it)
}
Stat.netBBPer100Hands(bbSum, totalHands)?.let { 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)
}
// 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) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
}
var average = 0.0 // also used for standard deviation later
if (computables.size > 0) {
average = ratedNet / computables.size.toDouble()
average = sum / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats(
setOf(
ComputedStat(AVERAGE, average),
ComputedStat(WIN_RATIO, winRatio),
ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio),
ComputedStat(AVERAGE_BUYIN, avgBuyin)
)
)
@ -348,7 +359,6 @@ class Calculator {
var longestWinStreak = 0
var longestLoseStreak = 0
var currentStreak = 0
var tITMCount = 0
computables.forEach { computable ->
index++
@ -356,7 +366,6 @@ class Calculator {
tBBSum += computable.bbNet
tBBSessionCount += computable.hasBigBlind
tWinningSessionCount += computable.isPositive
tITMCount += computable.isPositive
tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands
@ -382,16 +391,11 @@ class Calculator {
results.addEvolutionValue(tSum / index, stat = AVERAGE, 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, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO,
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(computable.ratedNet, stat = STANDARD_DEVIATION, data = session)
@ -433,9 +437,9 @@ class Calculator {
}
}
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty()
|| options.progressValues != Options.ProgressValues.NONE
|| options.computeDaysPlayed
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() ||
options.progressValues != Options.ProgressValues.NONE ||
options.computeDaysPlayed
// Session Set
if (shouldIterateOverSets) {
@ -482,6 +486,10 @@ class Calculator {
)
results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet)
Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = sessionSet)
}
}
Options.ProgressValues.TIMED -> {
results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet)
@ -533,7 +541,7 @@ class Calculator {
var hourlyRate = 0.0
if (gHourlyDuration != null) {
hourlyRate = ratedNet / gHourlyDuration
hourlyRate = sum / gHourlyDuration
if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate)
@ -619,10 +627,10 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet)
} else {
ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet }
ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumByDouble { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumOf { it.estimatedHands }
estimatedHands = filteredSessions.sumByDouble { it.estimatedHands }
}
}
}

@ -1,111 +0,0 @@
package net.pokeranalytics.android.calculus
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import timber.log.Timber
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The size of the retrieved computables list
*/
var size: Int = 0
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this.size = computables.size
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
this._sessionSets = 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() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
}

@ -1,13 +1,25 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import com.github.mikephil.charting.data.*
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry
import net.pokeranalytics.android.ui.graph.Graph
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.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import net.pokeranalytics.android.util.TextFormat
import kotlin.math.abs
/**
* The class returned after performing calculation in the Calculator object
@ -31,28 +43,138 @@ class Report(var options: Calculator.Options) {
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
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: options.stats.firstOrNull()
val statName = statToUse?.name ?: ""
statToUse?.let {
this._results.forEachIndexed { index, results ->
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.firstOrNull()
statToUse?.let {
this._results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
}
}
}
return if (count >= 2) { computedResults } else { null }
val label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this._results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}
}
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(var query: Query, var stats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
this._sessionSets = sets
return sets
}
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}
class ComputedResults(group: ComputableGroup,
@ -74,10 +196,6 @@ class ComputedResults(group: ComputableGroup,
private val allStats: Collection<ComputedStat>
get() { return this._computedStats.values }
val evolutionValues: Map<Stat, MutableList<Point>>
get() {
return this._evolutionValues
}
/**
* Adds a value to the evolution values
@ -237,6 +355,79 @@ class ComputedResults(group: ComputableGroup,
this.consolidateProgressStats()
}
// MPAndroidChart
fun defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}
val isEmpty: Boolean
get() {
return this.group.isEmpty
@ -258,18 +449,18 @@ class ComputedResults(group: ComputableGroup,
override fun legendValues(
stat: Stat,
total: Double,
style: Graph.Style,
entry: Entry,
style: GraphFragment.Style,
groupName: String,
context: Context
): LegendContent {
when (style) {
Graph.Style.BAR -> {
GraphFragment.Style.BAR -> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(total, currency = null)
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue)
}
else -> {
@ -283,12 +474,12 @@ class ComputedResults(group: ComputableGroup,
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(total, currency = null)
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(total, currency = null)
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue)
}
}
@ -298,3 +489,9 @@ class ComputedResults(group: ComputableGroup,
}
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}

@ -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.exceptions.FormattingException
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.RowViewType
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
*/
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable, PerformanceKey {
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable {
NET_RESULT(1),
BB_NET_RESULT(2),
@ -45,8 +44,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED(18),
LOCATIONS_PLAYED(19),
LONGEST_STREAKS(20),
MAXIMUM_NET_RESULT(21),
MINIMUM_NET_RESULT(22),
MAXIMUM_NETRESULT(21),
MINIMUM_NETRESULT(22),
MAXIMUM_DURATION(23),
DAYS_PLAYED(24),
WINNING_SESSION_COUNT(25),
@ -54,8 +53,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
TOTAL_BUYIN(27),
RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31)
;
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?
get() {
return when (this) {
@ -129,13 +124,11 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED -> R.string.number_of_hands
LOCATIONS_PLAYED -> R.string.locations_played
LONGEST_STREAKS -> R.string.longest_streaks
MAXIMUM_NET_RESULT -> R.string.max_net_result
MINIMUM_NET_RESULT -> R.string.min_net_result
MAXIMUM_NETRESULT -> R.string.max_net_result
MINIMUM_NETRESULT -> R.string.min_net_result
MAXIMUM_DURATION -> R.string.longest_session
DAYS_PLAYED -> R.string.days_played
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")
}
}
@ -152,7 +145,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
when (this) {
// 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
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 -> {
return TextFormat(value.formattedHourlyDuration())
} // 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
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
return TextFormat("${(value * 100).formatted}%", color)
}
// white amounts
// white amountsr
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))
}
LONGEST_STREAKS -> {
@ -192,7 +185,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() {
return when (this) {
RISK_OF_RUIN -> 5.0
TOURNAMENT_ITM_RATIO -> 10.0
WIN_RATIO -> 50.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
NUMBER_OF_SETS -> R.string.number_of_sessions
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_BB -> R.string.average_net_result_bb_
STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
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
}
resId?.let {
@ -247,8 +239,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() {
return when (this) {
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, HANDS_PLAYED,
STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_TIPS -> false
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS -> false
else -> true
}
}
@ -265,7 +257,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val legendHideRightValue: Boolean
get() {
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
else -> false
}
@ -277,8 +269,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val graphSignificantIndividualValue: Boolean
get() {
return when (this) {
AVERAGE, WIN_RATIO, TOURNAMENT_ITM_RATIO,
NUMBER_OF_SETS, NUMBER_OF_GAMES,
AVERAGE, WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES,
STANDARD_DEVIATION, HOURLY_DURATION -> false
else -> true
}
@ -324,8 +315,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return when (this) {
NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
AVERAGE_HOURLY_DURATION, HOURLY_DURATION,
NUMBER_OF_SETS, ROI, AVERAGE_BUYIN,
WIN_RATIO, TOURNAMENT_ITM_RATIO,
NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, WIN_RATIO,
AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true
else -> false
}

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

@ -13,6 +13,7 @@ import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
import kotlin.collections.HashMap
/**
* This class holds the results from the BankrollCalculator computations
@ -63,7 +64,6 @@ class BankrollReport(var setup: BankrollReportSetup) {
*/
private fun computeBankrollTotal() {
this.total = this.initial + this.netResult + this.transactionsNet
// Timber.d("init = $initial, net = $netResult, trans = $transactionsNet")
}
/**
@ -220,10 +220,14 @@ class BankrollReportSetup(val bankrollId: String? = null, val from: Date? = null
query.add(bankrollCondition)
}
this.from?.let {
query.add(QueryCondition.StartedFromDate(it))
val fromCondition = QueryCondition.StartedFromDate()
fromCondition.singleValue = it
query.add(fromCondition)
}
this.to?.let {
query.add(QueryCondition.StartedToDate(it))
val toCondition = QueryCondition.StartedToDate()
toCondition.singleValue = it
query.add(toCondition)
}
return query
}

@ -1,12 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.ui.graph.Graph
val AggregationType.axisFormatting: Graph.AxisFormatting
get() {
return when (this) {
AggregationType.DURATION -> Graph.AxisFormatting.X_DURATION
else -> Graph.AxisFormatting.DEFAULT
}
}

@ -1,84 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import com.github.mikephil.charting.data.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Point
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import kotlin.math.abs
// MPAndroidChart
fun ComputedResults.defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun ComputedResults.singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun ComputedResults.durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this.evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this.evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}

@ -1,64 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
/**
* The way the computed stats are going to be displayed
*/
enum class ReportDisplay : RowRepresentable {
TABLE,
PROGRESS,
COMPARISON,
MAP;
override val resId: Int?
get() {
return when (this) {
TABLE -> R.string.table
PROGRESS -> R.string.progress
COMPARISON -> R.string.comparison
MAP -> R.string.map
}
}
val activityClass: Class<*>
get() {
return when (this) {
TABLE -> TableReportActivity::class.java
PROGRESS -> ProgressReportActivity::class.java
COMPARISON -> ComparisonReportActivity::class.java
else -> throw PAIllegalStateException("undefined activity for report display")
}
}
val progressValues: Calculator.Options.ProgressValues
get() {
return when (this) {
PROGRESS, COMPARISON -> Calculator.Options.ProgressValues.STANDARD
else -> Calculator.Options.ProgressValues.NONE
}
}
// val requireProgressValues: Boolean
// get() {
// return when (this) {
// PROGRESS, COMPARISON -> true
// else -> false
// }
// }
val multipleStatSelection: Boolean
get() {
return when (this) {
PROGRESS -> false
else -> true
}
}
}

@ -1,64 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.ColorUtils
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun Report.lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: this.options.stats.firstOrNull()
val statName = statToUse?.name ?: ""
statToUse?.let {
this.results.forEachIndexed { index, results ->
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun Report.barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.firstOrNull()
statToUse?.let {
this.results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
}
}
}
val label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun Report.multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this.results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}

@ -1,81 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
class StatRepresentable(var stat: Stat) : RowRepresentable {
companion object {
fun resId(stat: Stat): Int {
return when (stat) {
Stat.NET_RESULT -> R.string.net_result
Stat.BB_NET_RESULT -> R.string.total_net_result_bb_
Stat.HOURLY_RATE -> R.string.average_hour_rate
Stat.AVERAGE -> R.string.average
Stat.NUMBER_OF_SETS -> R.string.number_of_sessions
Stat.NUMBER_OF_GAMES -> R.string.number_of_records
Stat.HOURLY_DURATION -> R.string.duration
Stat.AVERAGE_HOURLY_DURATION -> R.string.average_hours_played
Stat.NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
Stat.HOURLY_RATE_BB -> R.string.average_hour_rate_bb_
Stat.AVERAGE_NET_BB -> R.string.average_net_result_bb_
Stat.WIN_RATIO -> R.string.win_ratio
Stat.AVERAGE_BUYIN -> R.string.average_buyin
Stat.ROI -> R.string.tournament_roi
Stat.STANDARD_DEVIATION -> R.string.standard_deviation
Stat.STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
Stat.STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation
Stat.HANDS_PLAYED -> R.string.number_of_hands
Stat.LOCATIONS_PLAYED -> R.string.locations_played
Stat.LONGEST_STREAKS -> R.string.longest_streaks
Stat.MAXIMUM_NET_RESULT -> R.string.max_net_result
Stat.MINIMUM_NET_RESULT -> R.string.min_net_result
Stat.MAXIMUM_DURATION -> R.string.longest_session
Stat.DAYS_PLAYED -> R.string.days_played
Stat.TOTAL_BUYIN -> R.string.total_buyin
else -> throw PAIllegalStateException("Stat ${stat.name} name required but undefined")
}
}
fun localizedTitle(stat: Stat, context: Context): String {
return context.getString(resId(stat))
}
}
override val resId: Int
get() {
return resId(this.stat)
}
/**
* Returns a label used to display the legend right value, typically a total or an average
*/
fun cumulativeLabelResId(context: Context): String {
val resId = when (this.stat) {
Stat.AVERAGE, Stat.AVERAGE_HOURLY_DURATION, Stat.NET_BB_PER_100_HANDS,
Stat.HOURLY_RATE_BB, Stat.AVERAGE_NET_BB, Stat.ROI, Stat.HOURLY_RATE -> R.string.average
Stat.NUMBER_OF_SETS -> R.string.number_of_sessions
Stat.NUMBER_OF_GAMES -> R.string.number_of_records
Stat.NET_RESULT -> R.string.total
Stat.STANDARD_DEVIATION -> R.string.net_result
Stat.STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
Stat.STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
Stat.WIN_RATIO, Stat.HOURLY_DURATION -> return this.localizedTitle(context)
else -> null
}
resId?.let {
return context.getString(it)
} ?: run {
return NULL_TEXT
}
}
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}

@ -21,163 +21,150 @@ import kotlin.math.round
*/
class CashGameOptimalDurationCalculator {
companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval =
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 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
*/
fun start(isLive: Boolean): Double? {
val realm = Realm.getDefaultInstance()
val query = Query().add(QueryCondition.IsCash) // cash game
query.add(
if (isLive) {
QueryCondition.IsLive
} else {
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")
dur
}
// define validity interval
var start: Double? = null
var end: Double? = null
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
// Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
start = key
}
if (sessionCount >= minimumValidityCount) {
end = key
validBuckets++
}
}
// Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
// Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
// Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
val options = Calculator.Options()
options.query = query
val report = Calculator.computeStats(realm, options)
val stdBB =
report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
val p = polynomialRegression(sessions, stdBB)
var bestAverage = 0.0
var bestHourlyRate = 0.0
var bestDuration = 0.0
var maxDuration = 0.0
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
val averageResult = getBB(duration, p)
val hourly = averageResult / duration
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) {
bestAverage = averageResult
bestDuration = duration
}
if (duration > 0 && hourly > bestHourlyRate) {
bestHourlyRate = hourly
}
if (duration > maxDuration) {
maxDuration = duration
}
}
}
if (bestDuration > 0.0) {
return bestDuration
}
// Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
var y = 0.0
for (i in polynomial.indices) {
y += polynomial[i] * netDuration.pow(i)
}
return y
}
private fun polynomialRegression(
sessions: List<Session>,
bbStandardDeviation: Double?
): DoubleArray {
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
val points = WeightedObservedPoints()
val now = Date().time
sessions.forEach {
var weight = 5.0
val endTime = it.endDate?.time ?: 0L
val age = now - endTime
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point
weight -= 1.0
}
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points
weight -= 3.0
}
points.add(weight, it.netDuration.toDouble(), it.bbNet)
}
companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval = 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 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
*/
fun start(isLive: Boolean): Double? {
val realm = Realm.getDefaultInstance()
val query = Query().add(QueryCondition.IsCash) // cash game
query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online
query.add(QueryCondition.EndDateNotNull) // ended
query.add(QueryCondition.BigBlindNotNull) // 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")
dur
}
// define validity interval
var start: Double? = null
var end: Double? = null
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
start = key
}
if (sessionCount >= minimumValidityCount) {
end = key
validBuckets++
}
}
Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
val options = Calculator.Options()
options.query = query
val report = Calculator.computeStats(realm, options)
val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
val p = polynomialRegression(sessions, stdBB)
var bestAverage = 0.0
var bestHourlyRate = 0.0
var bestDuration = 0.0
var maxDuration = 0.0
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
val averageResult = getBB(duration, p)
val hourly = averageResult / duration
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) {
bestAverage = averageResult
bestDuration = duration
}
if (duration > 0 && hourly > bestHourlyRate) {
bestHourlyRate = hourly
}
if (duration > maxDuration){
maxDuration = duration
}
}
}
if (bestDuration > 0.0) {
return bestDuration
}
Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
var y = 0.0
for (i in polynomial.indices) {
y += polynomial[i] * netDuration.pow(i)
}
return y
}
private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray {
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
val points = WeightedObservedPoints()
val now = Date().time
sessions.forEach {
var weight = 5.0
val endTime = it.endDate?.time ?: 0L
val age = now - endTime
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point
weight -= 1.0
}
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points
weight -= 3.0
}
points.add(weight, it.netDuration.toDouble(), it.bbNet)
}
// polynomial of 7 degree, same as iOS
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
}
// polynomial of 7 degree, same as iOS
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
}
}
}
}

@ -1,6 +1,6 @@
package net.pokeranalytics.android.exceptions
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
class ModelException(message: String) : Exception(message)
class FormattingException(message: String) : Exception(message)
@ -14,19 +14,16 @@ class PAIllegalStateException(message: String) : Exception(message)
sealed class PokerAnalyticsException(message: String) : Exception(message) {
object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition")
// object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition")
// object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized")
// object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
class QueryValueMapUnknown(message: String = "fieldName is missing"): PokerAnalyticsException(message)
class QueryTypeUnhandled(clazz: String) :
PokerAnalyticsException(message = "queryWith type not handled: $clazz")
class ComparisonCriteriaUnhandled(criteria: Criteria) :
PokerAnalyticsException(message = "Criteria type not handled: ${criteria.uniqueIdentifier}")
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 FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing")
object QueryTypeUnhandled: PokerAnalyticsException(message = "queryWith type not handled")
object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected")
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 QueryValueMapMissingKeys(val missingKeys: List<String>) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys")
// data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow")
// data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
// object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment")
data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "queryWith element '$filterElementRow' type is missing")
data class QueryValueMapMissingKeys(val missingKeys: List<String>) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys")
data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow")
data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment")
}

@ -7,10 +7,10 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
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.Limits.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.TournamentFeatures.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 {
objects.add(QueryCondition.CustomFieldListQuery(it))
}
objects.sort()
objects.sorted()
realm.close()
return objects.map { Query(it) }
}
@ -104,10 +104,11 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
}
objects.add(condition)
}
objects.sort()
objects.sorted()
return objects.map { Query(it) }
}
QueryCondition.distinct<Session, T, S>()?.let {
val values = it.mapNotNull { session ->
when (this) {
@ -123,8 +124,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is TournamentFees -> if (session.tournamentEntryFee is S) {
session.tournamentEntryFee as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is Stakes -> if (session.cgStakes is S) {
session.cgStakes as S
is Blinds -> if (session.blinds is S) {
session.blinds as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
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 Years : ListCriteria(15)
object AllMonthsUpToNow : ListCriteria(16)
object Stakes : ListCriteria(17)
object Blinds : ListCriteria(17)
object TournamentFees : ListCriteria(18)
object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
val queries: List<Query>
get() {
@ -237,34 +237,19 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.close()
years
}
is Stakes -> comparison<QueryCondition.AnyStake, String>()
is Blinds -> comparison<QueryCondition.AnyBlind, String>()
is ListCustomFields -> comparison<CustomFieldEntry>()
is ValueCustomFields -> {
val realm = Realm.getDefaultInstance()
val queries = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
else -> throw PokerAnalyticsException.QueryTypeUnhandled
}
realm.close()
queries
}
is Duration -> {
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)
else -> throw PokerAnalyticsException.QueryTypeUnhandled
}
}
@ -286,7 +271,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
DayPeriods -> R.string.weekdays_or_weekend
Years -> R.string.year
AllMonthsUpToNow -> R.string.month
Stakes -> R.string.blind
Blinds -> R.string.blind
TournamentFees -> R.string.entry_fees
// is ListCustomFields -> 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> {
val objects = mutableListOf<S>()
val realm = Realm.getDefaultInstance()
realm.where<T>().sort("name").findAll().forEach {
realm.where<T>().findAll().forEach {
val condition = (QueryCondition.getInstance<T>() as S).apply {
setObject(it)
}
objects.add(condition)
}
// objects.sort()
objects.sorted()
realm.close()
return objects.map { Query(it) }
}
@ -318,7 +303,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
}
objects.add(condition)
}
objects.sort()
objects.sorted()
return objects.map { Query(it) }
}
@ -334,8 +319,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
TournamentFeatures, Limits, TableSizes, TournamentTypes,
MonthsOfYear, DaysOfWeek, SessionTypes,
BankrollTypes, DayPeriods, Years,
AllMonthsUpToNow,
Stakes, TournamentFees
AllMonthsUpToNow, Blinds, 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,10 +2,10 @@ package net.pokeranalytics.android.model.extensions
import android.content.Context
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.NotificationSchedule
@ -36,17 +36,17 @@ enum class SessionState {
fun Session.getState(): SessionState {
val start = this.startDate
return if (start == null) {
SessionState.PENDING
if (start == null) {
return SessionState.PENDING
} else {
if (start > Date()) {
SessionState.PLANNED
return SessionState.PLANNED
} else if (this.endDate != null) {
SessionState.FINISHED
return SessionState.FINISHED
} else if (this.pauseDate != null) {
SessionState.PAUSED
return SessionState.PAUSED
} else {
SessionState.STARTED
return SessionState.STARTED
}
}
}
@ -76,8 +76,8 @@ fun Session.getFormattedGameType(context: Context): String {
parameters.add(context.getString(R.string.tournament).capitalize())
}
} else {
if (this.cgAnte != null || this.cgBlinds != null) {
parameters.add(getFormattedStakes())
if (cgSmallBlind != null && cgBigBlind != null) {
parameters.add(getFormattedBlinds())
}
game?.let {
parameters.add(getFormattedGame())
@ -103,9 +103,7 @@ fun Session.cancelStopNotification(context: Context) {
fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
val timeDelay = optimalDuration - this.currentDuration()
if (timeDelay <= 0) {
return
}
if (timeDelay <= 0) throw PAIllegalStateException("A stop notif has been setup for the past: start=${this.startDate}, end=${this.endDate}, optimalDuration=$optimalDuration")
val title = context.getString(R.string.stop_notification_title)
val body = context.getString(R.string.stop_notification_body)
@ -120,10 +118,11 @@ fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
.addTag(this.id)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
WorkManager.getInstance(context).enqueue(work)
}
val AbstractList<Session>.hourlyDuration: Double
get() {
val intervals = mutableListOf<TimeInterval>()
@ -131,7 +130,7 @@ val AbstractList<Session>.hourlyDuration: Double
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval)
}
return intervals.sumOf { it.hourlyDuration }
return intervals.sumByDouble { it.hourlyDuration }
}
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {

@ -1,10 +1,10 @@
package net.pokeranalytics.android.model.filter
import com.crashlytics.android.Crashlytics
import io.realm.RealmModel
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.CrashLogging
/**
* We want to be able to store filters in the database:
@ -65,7 +65,7 @@ class FilterHelper {
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition)
else -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
Crashlytics.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null
// throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}")
}

@ -11,13 +11,9 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query {
constructor(query: Query) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) {
constructor(vararg elements: QueryCondition) {
if (elements.isNotEmpty()) {
this.add(elements.filterNotNull())
this.add(elements.asList())
}
}
@ -39,7 +35,7 @@ class Query {
return this
}
fun add(queryConditions: List<QueryCondition>): Query {
fun add(queryConditions: List<QueryCondition>): Query{
this._conditions.addAll(queryConditions)
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) {
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) }
}
}
@ -88,14 +84,14 @@ class Query {
}
// println("<<<<<< ${realmQuery.description}")
// val queryLast = this.conditions.firstOrNull {
// it is QueryCondition.Last
// }
// queryLast?.let {qc ->
// (qc as QueryCondition.Last).singleValue?.let {
// return realmQuery.limit(it.toLong())
// }
// }
val queryLast = this.conditions.firstOrNull {
it is QueryCondition.Last
}
queryLast?.let {qc ->
(qc as QueryCondition.Last).singleValue?.let {
return realmQuery.limit(it.toLong())
}
}
return realmQuery
}
@ -104,25 +100,4 @@ class Query {
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,73 +12,45 @@ import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize
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.NameManageable
import net.pokeranalytics.android.model.interfaces.StakesHolder
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.text.DateFormatSymbols
import java.text.NumberFormat
import java.util.*
import kotlin.reflect.KClass
import kotlin.collections.ArrayList
/**
* Enum describing the way a query should be handled
* Some queries requires a value to be checked upon through equals, in, more, less, between
*/
sealed class QueryCondition : RowRepresentable {
companion object {
fun <T: Any> newInstance(kClass: KClass<T>): T {
return try {
kClass.objectInstance ?: kClass.java.newInstance()
} catch (e: Exception) {
// some object instance can fail due to: java.lang.Class has no zero argument constructor
// We should have just one constructor with a single parameter
val primaryConstructor = kClass.java.declaredConstructors.first()
val paramClass = primaryConstructor.parameterTypes.first()
val param = when (paramClass) {
Int::class.java -> 0
Double::class.java -> 0.0
else -> paramClass.newInstance()
}
val constructor = kClass.java.getDeclaredConstructor(paramClass)
constructor.newInstance(param)
}
}
// inline fun <reified T : QueryCondition> more(): T {
// 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 }
// }
sealed class QueryCondition : FilterElementRow {
inline fun <reified T : QueryCondition> moreOrEqual(): T {
return newInstance(T::class).apply { this.operator = Operator.MORE_OR_EQUAL }
companion object {
inline fun <reified T : QueryCondition> more(): T {
return T::class.java.newInstance().apply { this.operator = Operator.MORE }
}
inline fun <reified T : QueryCondition> lessOrEqual(): T {
return newInstance(T::class).apply { this.operator = Operator.LESS_OR_EQUAL }
inline fun <reified T : QueryCondition> less(): T {
return T::class.java.newInstance().apply { this.operator = Operator.LESS }
}
inline fun <reified T : QueryCondition> moreEqualOrLessEqual(): ArrayList<T> {
return arrayListOf(moreOrEqual(), lessOrEqual())
inline fun <reified T : QueryCondition> moreOrLess(): ArrayList<T> {
return arrayListOf(more(), less())
}
fun <T : QueryCondition> valueOf(name: String): T {
val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin
return newInstance(kClass) as T
val instance = kClass.objectInstance ?: kClass.java.newInstance()
return instance as T
}
inline fun <reified T : Identifiable> getInstance(): QueryCondition {
@ -89,14 +61,13 @@ sealed class QueryCondition : RowRepresentable {
TransactionType::class.java -> AnyTransactionType()
TournamentName::class.java -> AnyTournamentName()
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>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance()
realm.refresh()
val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
@ -114,9 +85,7 @@ sealed class QueryCondition : RowRepresentable {
ANY,
ALL,
MORE,
MORE_OR_EQUAL,
LESS,
LESS_OR_EQUAL,
EQUALS,
TRUE,
NOTNULL
@ -128,7 +97,7 @@ sealed class QueryCondition : RowRepresentable {
val groupId: String
get() {
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
}
}
@ -136,7 +105,7 @@ sealed class QueryCondition : RowRepresentable {
val id: List<String>
get() {
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 -> {}
}
@ -154,43 +123,24 @@ sealed class QueryCondition : RowRepresentable {
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 var listOfValues: MutableList<T>
abstract var listOfValues: ArrayList<T>
abstract fun labelForValue(value: T, context: Context): String
open fun entityName(context: Context): String {
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 {
val prefix = this.resId?.let {
context.getString(it) + " "
} ?: ""
return when (values.size) {
return when (listOfValues.size) {
0 -> return NULL_TEXT
else -> prefix + values.joinToString(", ") { labelForValue(it, context) }
// else -> "${values.size} $prefix ${entityName(context)}"
1, 2 -> prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ")
else -> "${listOfValues.size} $prefix ${entityName(context)}"
}
}
@ -201,41 +151,17 @@ sealed class QueryCondition : RowRepresentable {
fun firstValue(context: Context): String? {
return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) }
}
}
abstract class SingleValue<T>(value: T) : QueryCondition() where T : Comparable<T> {
var singleValue: T = value
abstract fun labelForValue(value: T, context: Context): String
override fun getDisplayName(context: Context): String {
return this.resId?.let {
context.getString(it)
} ?: ""
// return getDisplayName(context, this.singleValue)
}
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.singleValue)
}
open fun getDisplayName(context: Context, value: T): String {
val prefix = this.resId?.let {
context.getString(it)
} ?: ""
return prefix + " " + labelForValue(value, context)
}
abstract class SingleValue<T> : ListOfValues<T>() where T : Comparable<T> {
override var listOfValues = ArrayList<T>()
abstract var singleValue: T?
}
abstract class ListOfDouble : ListOfValues<Double>() {
open var sign: Int = 1
override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<Double>()
override var listOfValues: ArrayList<Double> = arrayListOf()
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues()
@ -248,7 +174,7 @@ sealed class QueryCondition : RowRepresentable {
abstract class ListOfInt : ListOfValues<Int>() {
override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<Int>()
override var listOfValues: ArrayList<Int> = arrayListOf()
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues()
@ -261,8 +187,7 @@ sealed class QueryCondition : RowRepresentable {
abstract class ListOfString : ListOfValues<String>() {
override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<String>()
override var listOfValues = ArrayList<String>()
override fun labelForValue(value: String, context: Context): String {
return value
}
@ -273,34 +198,56 @@ sealed class QueryCondition : RowRepresentable {
}
}
abstract class SingleDate(date: Date) : SingleValue<Date>(date) {
abstract class SingleDate : SingleValue<Date>() {
override fun labelForValue(value: Date, context: Context): String {
return value.shortDate()
}
// override var listOfValues = mutableListOf<Date>()
override var listOfValues = ArrayList<Date>()
override var singleValue: Date?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue()
}
}
abstract class SingleInt(value: Int) : SingleValue<Int>(value) {
abstract class SingleInt : SingleValue<Int>() {
override fun labelForValue(value: Int, context: Context): String {
return value.toString()
}
override var singleValue: Int?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue()
}
}
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() {
fun setObject(dataObject: T) {
@ -316,7 +263,7 @@ sealed class QueryCondition : RowRepresentable {
val completeLabel = when (listOfValues.size) {
0 -> NULL_TEXT
1, 2 -> {
listOfValues.joinToString(", ") { labelForValue(realm, it) }
listOfValues.map { labelForValue(realm, it) }.joinToString(", ")
}
else -> "${listOfValues.size} $entityName"
}
@ -324,10 +271,6 @@ sealed class QueryCondition : RowRepresentable {
return completeLabel
}
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context)
}
open fun entityName(realm: Realm, context: Context): String {
return entityName(context)
}
@ -336,31 +279,28 @@ sealed class QueryCondition : RowRepresentable {
val query = realm.where(entity)
return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
}
val objectId: String?
get() {
return this.listOfValues.firstOrNull()
}
}
interface DateTime {
val showTime: Boolean
}
abstract class DateQuery(date: Date) : SingleDate(date), DateTime {
abstract class DateQuery : SingleDate(), DateTime {
override val showTime: Boolean = false
override fun labelForValue(value: Date, context: Context): String {
return if (showTime) {
singleValue.shortTime()
} else {
singleValue.shortDate()
}
return singleValue?.let {
if (showTime) {
it.shortTime()
} else {
it.shortDate()
}
} ?: NULL_TEXT
}
}
abstract class TimeQuery(date: Date) : DateQuery(date) {
abstract class TimeQuery : DateQuery() {
override val showTime: Boolean = true
}
@ -403,34 +343,50 @@ sealed class QueryCondition : RowRepresentable {
}
}
class AnyTournamentName : QueryDataCondition<TournamentName>() {
class AnyTournamentName() : QueryDataCondition<TournamentName>() {
override val entity: Class<TournamentName> = TournamentName::class.java
constructor(tournamentName: TournamentName) : this() {
this.setObject(tournamentName)
}
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_names)
}
}
class AnyTournamentFeature : QueryDataCondition<TournamentFeature>() {
class AnyTournamentFeature() : QueryDataCondition<TournamentFeature>() {
override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature) : this() {
this.setObject(tournamentFeature)
}
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_features)
}
}
class AllTournamentFeature : QueryDataCondition<TournamentFeature>() {
class AllTournamentFeature() : QueryDataCondition<TournamentFeature>() {
override var operator = Operator.ALL
override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature) : this() {
this.setObject(tournamentFeature)
}
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_features)
}
}
class AnyLocation : QueryDataCondition<Location>() {
class AnyLocation() : QueryDataCondition<Location>() {
override val entity: Class<Location> = Location::class.java
constructor(location: Location) : this() {
this.setObject(location)
}
override fun entityName(context: Context): String {
return context.getString(R.string.locations)
}
@ -478,21 +434,19 @@ sealed class QueryCondition : RowRepresentable {
}
}
class AnyStake : ListOfString() {
override fun labelForValue(value: String, context: Context): String {
return StakesHolder.readableStakes(value)
}
class AnyBlind : ListOfString() {
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()))
object Last : SingleInt() {
override var operator = Operator.EQUALS
override fun getDisplayName(context: Context): String {
//TODO update string "last %i"
return "${context.getString(R.string.last_i_records)} $singleValue"
}
}
}
class NumberOfTable : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
@ -522,13 +476,13 @@ sealed class QueryCondition : RowRepresentable {
}
override fun labelForValue(value: Int, context: Context): String {
val suffix = when (value % 10) {
val suffix = when (value%10) {
1 -> context.getString(R.string.ordinal_suffix_first)
2 -> context.getString(R.string.ordinal_suffix_second)
3 -> context.getString(R.string.ordinal_suffix_third)
else -> context.getString(R.string.ordinal_suffix_default)
}
return "$value$suffix " + context.getString(R.string.position)
return "$value$suffix "+context.getString(R.string.position)
}
override fun entityName(context: Context): String {
@ -545,7 +499,7 @@ sealed class QueryCondition : RowRepresentable {
class TournamentNumberOfPlayer : ListOfInt() {
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 {
@ -553,20 +507,20 @@ sealed class QueryCondition : RowRepresentable {
}
}
class StartedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
class StartedFromDate : DateQuery() {
override var operator = Operator.MORE
}
class StartedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
class StartedToDate : DateQuery() {
override var operator = Operator.LESS
}
class EndedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
class EndedFromDate : DateQuery() {
override var operator = Operator.MORE
}
class EndedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
class EndedToDate : DateQuery() {
override var operator = Operator.LESS
}
class AnyDayOfWeek : ListOfInt() {
@ -576,23 +530,23 @@ sealed class QueryCondition : RowRepresentable {
}
class AnyMonthOfYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize()
}
constructor(month: Int) : this() {
listOfValues = arrayListOf(month)
}
}
class AnyYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize()
return "$value"
}
}
class AnyYear() : ListOfInt() {
constructor(year: Int) : this() {
listOfValues = arrayListOf(year)
}
override fun labelForValue(value: Int, context: Context): String {
return "$value"
}
}
object IsWeekDay : TrueQueryCondition()
@ -610,19 +564,24 @@ sealed class QueryCondition : RowRepresentable {
}
}
class PastDay(value: Int) : SingleInt(value) {
class PastDay : SingleInt() {
override var operator = Operator.EQUALS
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
override fun getDisplayNameWithValues(context: Context): String {
return context.getString(R.string.period_in_days_with_value, this.singleValue.toString())
override fun labelForValue(value: Int, context: Context): String {
return value.toString()
}
override fun entityName(context: Context): String {
return this.resId?.let {
" " + context.getString(it)
} ?: ""
}
}
class Duration(value: Int) : SingleInt(value) {
class Duration : SingleInt() {
override var operator = Operator.EQUALS
var minutes: Int
var minutes: Int?
get() {
return singleValue
}
@ -630,9 +589,12 @@ sealed class QueryCondition : RowRepresentable {
singleValue = value
}
val netDuration: Long
val netDuration: Long?
get() {
return (singleValue * 60 * 1000).toLong()
minutes?.let {
return (it * 60 * 1000).toLong()
}
return null
}
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
@ -645,14 +607,24 @@ sealed class QueryCondition : RowRepresentable {
object DateNotNull : NotNullQueryCondition()
object EndDateNotNull : NotNullQueryCondition()
object BiggestBetNotNull : NotNullQueryCondition()
object BigBlindNotNull : NotNullQueryCondition()
class StartedFromTime() : TimeQuery() {
override var operator = Operator.MORE
constructor(date: Date) : this() {
singleValue = date
}
class StartedFromTime(date: Date) : TimeQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
}
class EndedToTime(date: Date) : TimeQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
class EndedToTime() : TimeQuery() {
override var operator = Operator.LESS
constructor(date: Date) : this() {
singleValue = date
}
}
interface CustomFieldRelated {
@ -667,8 +639,12 @@ sealed class QueryCondition : RowRepresentable {
}
}
class CustomFieldQuery : QueryDataCondition<CustomField>() {
class CustomFieldQuery() : QueryDataCondition<CustomField>() {
override var entity: Class<CustomField> = CustomField::class.java
constructor(customField: CustomField) : this() {
this.setObject(customField)
}
}
open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated {
@ -690,9 +666,7 @@ sealed class QueryCondition : RowRepresentable {
val completeLabel = when (listOfValues.size) {
0 -> return NULL_TEXT
1, 2 -> {
return name + prefix + listOfValues.joinToString(", ") {
labelForValue(it, context)
}
return name + prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ")
}
else -> "${listOfValues.size} $prefix $name"
}
@ -748,16 +722,9 @@ sealed class QueryCondition : RowRepresentable {
): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
// if (BuildConfig.DEBUG) {
// val className = T::class.java
// 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")
if (BuildConfig.DEBUG) {
fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
}
fieldName ?: return realmQuery
when (this) {
@ -795,12 +762,15 @@ sealed class QueryCondition : RowRepresentable {
.lessThanOrEqualTo(fieldName, calendar.time.endOfDay())
}
is PastDay -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.DAY_OF_YEAR, -singleValue)
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
.lessThanOrEqualTo(fieldName, startDate.endOfDay())
singleValue?.let {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.DAY_OF_YEAR, -it)
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
.lessThanOrEqualTo(fieldName, startDate.endOfDay())
}
return realmQuery
}
is DuringThisWeek -> {
val calendar = Calendar.getInstance()
@ -830,25 +800,32 @@ sealed class QueryCondition : RowRepresentable {
}
is StartedFromTime -> {
val calendar = Calendar.getInstance()
calendar.time = singleValue
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is EndedToTime) {
calendar.time = otherQueryCondition.singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
singleValue?.let {
calendar.time = it
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is EndedToTime) {
otherQueryCondition.singleValue?.let { endTime ->
calendar.time = endTime
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
}
}
}
return realmQuery
}
is EndedToTime -> {
val calendar = Calendar.getInstance()
calendar.time = singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is StartedFromTime) {
calendar.time = otherQueryCondition.singleValue
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
singleValue?.let {
calendar.time = singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is StartedFromTime) {
otherQueryCondition.singleValue?.let { startTime ->
calendar.time = startTime
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
}
}
}
return realmQuery
}
else -> {}
}
if (this is CustomFieldRelated) {
@ -869,62 +846,58 @@ sealed class QueryCondition : RowRepresentable {
return when (operator) {
Operator.EQUALS -> {
when (this) {
is SingleDate -> realmQuery.equalTo(fieldName, singleValue)
is SingleInt -> realmQuery.equalTo(fieldName, singleValue)
is SingleDate -> realmQuery.equalTo(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleInt -> realmQuery.equalTo(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign)
is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first())
else -> realmQuery
}
}
Operator.MORE_OR_EQUAL -> {
Operator.MORE -> {
when (this) {
is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue.startOfDay())
is SingleDate -> realmQuery.greaterThanOrEqualTo(
fieldName,
singleValue?.startOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.greaterThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is TournamentFinalPosition -> 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 ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first())
is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1)
is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.LESS_OR_EQUAL -> {
Operator.LESS -> {
when (this) {
is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue.endOfDay())
is SingleDate -> realmQuery.lessThanOrEqualTo(
fieldName,
singleValue?.endOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.lessThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is TournamentFinalPosition -> 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 ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first())
is NetAmountLost -> {
realmQuery.greaterThan(fieldName, listOfValues.first() * -1)
@ -980,8 +953,8 @@ sealed class QueryCondition : RowRepresentable {
is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> {
when (this.operator) {
Operator.MORE, Operator.MORE_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS, Operator.LESS_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> RowViewType.TITLE_CHECK.ordinal
}
}
@ -994,8 +967,8 @@ sealed class QueryCondition : RowRepresentable {
is PastDay -> BottomSheetType.EDIT_TEXT
else -> {
when (this.operator) {
Operator.MORE, Operator.MORE_OR_EQUAL -> BottomSheetType.EDIT_TEXT
Operator.LESS, Operator.LESS_OR_EQUAL -> BottomSheetType.EDIT_TEXT
Operator.MORE -> BottomSheetType.EDIT_TEXT
Operator.LESS -> BottomSheetType.EDIT_TEXT
else -> BottomSheetType.NONE
}
}
@ -1022,27 +995,27 @@ sealed class QueryCondition : RowRepresentable {
is IsWeekDay -> R.string.week_days
is IsWeekEnd -> R.string.weekend
is PastDay -> R.string.period_in_days
// is TournamentNumberOfPlayer -> {
// when (this.operator) {
// Operator.MORE -> R.string.minimum
// Operator.LESS -> R.string.maximum
// else -> null
// }
// }
// is NetAmountWon -> {
// when (this.operator) {
// Operator.MORE -> R.string.won_amount_more_than
// Operator.LESS -> R.string.won_amount_less_than
// else -> null
// }
// }
// is NetAmountLost -> {
// when (this.operator) {
// Operator.MORE -> R.string.lost_amount_more_than
// Operator.LESS -> R.string.lost_amount_less_than
// else -> null
// }
// }
is TournamentNumberOfPlayer -> {
when (this.operator) {
Operator.MORE -> R.string.minimum
Operator.LESS -> R.string.maximum
else -> null
}
}
is NetAmountWon -> {
when (this.operator) {
Operator.MORE -> R.string.won_amount_more_than
Operator.LESS -> R.string.won_amount_less_than
else -> null
}
}
is NetAmountLost -> {
when (this.operator) {
Operator.MORE -> R.string.lost_amount_more_than
Operator.LESS -> R.string.lost_amount_less_than
else -> null
}
}
is TournamentFinalPosition -> {
when (this.operator) {
Operator.MORE -> R.string.minimum
@ -1052,10 +1025,8 @@ sealed class QueryCondition : RowRepresentable {
}
else -> {
when (this.operator) {
Operator.MORE_OR_EQUAL -> R.string.more_or_equal_sign
Operator.MORE -> R.string.more_sign
Operator.LESS_OR_EQUAL -> R.string.less_or_equal_sign
Operator.LESS -> R.string.less_sign
Operator.MORE -> R.string.more_than
Operator.LESS -> R.string.less_than
else -> null
}
}

@ -0,0 +1,80 @@
package net.pokeranalytics.android.model.handhistory
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.handhistory.Card
/***
* An interface used for board changes notifications
*/
interface BoardChangedListener {
fun boardChanged()
}
/***
* The BoardManager purpose is to manage the cards from a hand history board
* and notify its listener when a change occurs
*/
class BoardManager(cards: List<Card>, var listener: BoardChangedListener) {
/***
* The sorted list of cards
*/
private var sortedBoardCards: MutableList<Card> = mutableListOf()
/***
* All cards
*/
val allCards: List<Card>
get() {
return this.sortedBoardCards
}
init {
this.sortedBoardCards = cards.sortedBy { it.index }.toMutableList()
}
/***
* Adds a card to the board, notifies the listener
*/
fun add(card: Card) {
this.sortedBoardCards.lastOrNull()?.let {
if (it.suit == null) {
it.suit = Card.Suit.UNDEFINED
}
}
if (this.sortedBoardCards.size == 5) {
throw PAIllegalStateException("Can't add anymore cards")
}
card.index = this.sortedBoardCards.size
this.sortedBoardCards.add(card)
this.listener.boardChanged()
}
/***
* Clears the street's cards, notifies the listener
*/
fun clearStreet(street: Street) {
this.sortedBoardCards.removeAll { it.street == street }
this.listener.boardChanged()
}
/***
* Returns the last card of a given [street]
*/
fun lastCard(street: Street) : Card? {
return this.sortedBoardCards.lastOrNull { it.street == street }
}
/***
* Remove the given [card], notifies the listener
*/
fun remove(card: Card) {
this.sortedBoardCards.remove(card)
this.listener.boardChanged()
}
}

@ -6,6 +6,7 @@ import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
import java.util.*
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) {
this.blinds = handHistory.blinds
this.smallBlind = handHistory.smallBlind
this.bigBlind = handHistory.bigBlind
this.bigBlindAnte = handHistory.bigBlindAnte
this.ante = handHistory.ante
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.type = session.sessionType
this.blinds = session.cgBlinds
this.ante = session.cgAnte
this.smallBlind = session.cgSmallBlind
this.bigBlind = session.cgBigBlind
this.tableSize = session.tableSize
}
val blindValues = session.blindValues
if (blindValues.size > 2) {
this.straddlePositions = Position.positionsPerPlayers(10).drop(2).take(blindValues.size - 2).toMutableList()
}
var type: Session.Type? = null
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()
}
/***

@ -5,7 +5,7 @@ import io.realm.RealmModel
/**
* An interface to be able to track the usage of an object
*/
interface UsageCountable : Identifiable {
interface CountableUsage : Identifiable {
var useCount: Int
get() { return 0 }
set(_) {}

@ -5,6 +5,7 @@ import io.realm.Realm
import io.realm.RealmModel
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.ui.view.RowRepresentable
enum class SaveValidityStatus {
VALID,
@ -22,7 +23,7 @@ enum class DeleteValidityStatus {
/**
* An interface to group object which are managed by the database
*/
interface Manageable : Savable, Deletable
interface Manageable : Savable, Deletable, Editable
interface NameManageable : Manageable {
var name: String
@ -67,6 +68,17 @@ interface Identifiable : RealmModel {
}
}
/**
* An interface to update the fields of an object
*/
interface Editable : Identifiable {
/**
* a method to handle the modification of the object.
* Through [RowRepresentable] the object is able to update the right variable with the new value.
*/
fun updateValue(value: Any?, row: RowRepresentable)
}
/**
* An interface to easily handle the validity of any object we want to save
*/

@ -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 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.QueryCondition
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.SessionSetManager
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.Preferences
import java.text.NumberFormat
class Patcher {
companion object {
fun patchAll(application: PokerAnalyticsApplication) {
val context = application.applicationContext
// NOTE: it's more than possible that at one point many patches become redundant
// with each other
fun patchAll(context: Context) {
patchMissingTransactionTypes(context)
Preferences.executeOnce(Preferences.Keys.PATCH_COMPUTABLE_RESULTS, context) {
patchComputableResults()
}
patchSessionSet()
Preferences.executeOnce(Preferences.Keys.PATCH_SESSION_SETS, context) {
patchSessionSet()
}
@ -39,42 +27,51 @@ class Patcher {
Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) {
patchDefaultTransactionTypes(context)
}
Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) {
patchStakes()
}
Preferences.executeOnce(Preferences.Keys.PATCH_NEGATIVE_LIMITS, context) {
patchNegativeLimits()
}
Preferences.executeOnce(Preferences.Keys.CLEAN_BLINDS_FILTERS, context) {
cleanBlindsFilters()
}
Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
patchMissingTransactionTypes(context)
Preferences.executeOnce(Preferences.Keys.PATCH_BLINDS_FORMAT, context) {
patchBlindFormat()
}
Preferences.executeOnce(Preferences.Keys.PATCH_ZERO_TABLE, context) {
patchZeroTable()
}
val realm = Realm.getDefaultInstance()
Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) {
patchRatedAmounts()
val lockedTypes =
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) {
val realm = Realm.getDefaultInstance()
val transactionTypes = TransactionType.Value.values()
realm.executeTransaction {
Seed.createDefaultTransactionTypes(transactionTypes, context, realm)
val depositType = TransactionType.Value.DEPOSIT
val deposit = realm.where(TransactionType::class.java)
.equalTo("kind", depositType.uniqueIdentifier).findFirst()
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()
}
private fun patchBreaks() {
@ -89,8 +86,7 @@ class Patcher {
it.computeStats()
}
sessions.forEach {
it.generateStakes()
it.defineHighestBet()
it.formatBlinds()
}
results.forEach {
it.computeNumberOfRebuy()
@ -115,50 +111,13 @@ class Patcher {
realm.close()
}
private fun patchStakes() {
private fun patchBlindFormat() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session ->
val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
session.cgAnte = null
if (blindsFormatted.isNotEmpty()) {
session.cgBlinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
session.formatBlinds()
}
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()
}
@ -180,55 +139,6 @@ class Patcher {
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 {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
// DynamicRealm exposes an editable schema
val schema = realm.schema
@ -21,8 +21,7 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 0) {
Timber.d("*** Running migration 1")
schema.get("Filter")?.addField("entityType", Int::class.java)
?.setNullable("entityType", true)
schema.get("Filter")?.addField("entityType", Int::class.java)?.setNullable("entityType", true)
schema.get("FilterElement")?.let {
it.setNullable("filterName", true)
it.setNullable("sectionName", true)
@ -84,8 +83,7 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 3) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Result")?.addField("numberOfRebuy", Double::class.java)
?.setNullable("numberOfRebuy", true)
schema.get("Result")?.addField("numberOfRebuy", Double::class.java)?.setNullable("numberOfRebuy", true)
currentVersion++
}
@ -119,8 +117,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("CustomField")?.let {
it.addField("type", Integer::class.java).setNullable("type", false)
it.addField("duplicateValue", Boolean::class.java)
it.addField("sortCondition", Integer::class.java)
.setRequired("sortCondition", true)
it.addField("sortCondition", Integer::class.java).setRequired("sortCondition", true)
it.addRealmListField("entries", customFieldEntrySchema)
}
@ -137,8 +134,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("ReportSetup")?.let {
it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true)
it.addRealmListField("criteriaCustomFieldIds", String::class.java)
it.addRealmListField("criteriaIds", Int::class.java)
.setNullable("criteriaIds", true)
it.addRealmListField("criteriaIds", Int::class.java).setNullable("criteriaIds", true)
it.removeField("filters")
schema.get("Filter")?.let { filterSchema ->
it.addRealmObjectField("filter", filterSchema)
@ -199,8 +195,7 @@ class PokerAnalyticsMigration : RealmMigration {
val cardSchema = schema.create("Card")
cardSchema.addField("value", Int::class.java).setRequired("value", false)
cardSchema.addField("suitIdentifier", Int::class.java)
.setRequired("suitIdentifier", false)
cardSchema.addField("suitIdentifier", Int::class.java).setRequired("suitIdentifier", false)
cardSchema.addField("index", Int::class.java)
hhSchema.addRealmListField("board", cardSchema)
@ -208,12 +203,10 @@ class PokerAnalyticsMigration : RealmMigration {
actionSchema.addField("streetIdentifier", Int::class.java)
actionSchema.addField("index", Int::class.java)
actionSchema.addField("position", Int::class.java)
actionSchema.addField("typeIdentifier", Int::class.java)
.setRequired("typeIdentifier", false)
actionSchema.addField("typeIdentifier", Int::class.java).setRequired("typeIdentifier", false)
actionSchema.addField("amount", Double::class.java).setRequired("amount", false)
actionSchema.addField("effectiveAmount", Double::class.java)
actionSchema.addField("positionRemainingStack", Double::class.java)
.setRequired("positionRemainingStack", false)
actionSchema.addField("positionRemainingStack", Double::class.java).setRequired("positionRemainingStack", false)
hhSchema.addRealmListField("actions", actionSchema)
val playerSetupSchema = schema.create("PlayerSetup")
@ -234,115 +227,13 @@ class PokerAnalyticsMigration : RealmMigration {
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 {
return RealmMigration::javaClass.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is RealmMigration
}
override fun hashCode(): Int {
return RealmMigration::javaClass.hashCode()
}
}

@ -14,37 +14,12 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.BankrollPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.ui.view.rowrepresentable.BankrollRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
enum class ResultCaptureType {
BUYIN_CASHED_OUT,
NET_RESULT;
companion object {
val buyinCashedOutFields = listOf(
SessionPropertiesRow.CASHED_OUT,
SessionPropertiesRow.BUY_IN,
SessionPropertiesRow.TIPS)
val netResultFields = listOf(SessionPropertiesRow.NET_RESULT)
}
val rowRepresentables: List<RowRepresentable>
get() {
return when (this) {
BUYIN_CASHED_OUT -> buyinCashedOutFields
NET_RESULT -> netResultFields
}
}
}
open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresentable {
open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -60,12 +35,6 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
@LinkingObjects("bankroll")
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
var currency: Currency? = null
@ -81,6 +50,25 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
return this.name
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollRow.LIVE -> {
this.live = if (value is Boolean) !value else false
}
BankrollRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty()
&& realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()
@ -112,16 +100,6 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
}
}
fun resultCaptureType(context: Context): ResultCaptureType {
return Preferences.getResultCaptureType(this, context)
?: run {
when (this.live) {
true -> ResultCaptureType.BUYIN_CASHED_OUT
else -> ResultCaptureType.NET_RESULT
}
}
}
companion object {
fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll {
@ -143,25 +121,6 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollPropertiesRow.ONLINE -> {
this.live = if (value is Boolean) !value else false
}
BankrollPropertiesRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollPropertiesRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollPropertiesRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
val utilCurrency: java.util.Currency
get() {
this.currency?.code?.let {

@ -13,12 +13,11 @@ import net.pokeranalytics.android.model.interfaces.Manageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
open class Comment : RealmObject(), Manageable, RowRepresentable, RowUpdatable {
open class Comment : RealmObject(), Manageable, RowRepresentable {
@PrimaryKey
override var id = UUID.randomUUID().toString()

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

@ -31,7 +31,7 @@ open class Currency : RealmObject() {
field = value
} catch (e: Exception) {
// make app crash earlier than later, possibly to show an error message to the user in the future
throw PAIllegalStateException(e.localizedMessage ?: e.toString())
throw PAIllegalStateException(e.localizedMessage)
}
}

@ -18,18 +18,17 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
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.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomFieldPropertiesRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomFieldRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource {
open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
companion object {
@ -112,9 +111,13 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
@Ignore
private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList()
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
@Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf()
//helper
val isListType: Boolean
@ -127,6 +130,14 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
return this.type == Type.AMOUNT.uniqueIdentifier
}
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
@ -134,8 +145,8 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
CustomFieldPropertiesRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldPropertiesRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
CustomFieldRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
}
}
@ -167,6 +178,13 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
return R.string.cf_entry_delete_popup_message
}
override val bottomSheetType: BottomSheetType
get() {
return when (type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun deleteDependencies(realm: Realm) {
if (isValid) {
@ -186,13 +204,35 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
return when (type) {
Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.add(CustomFieldPropertiesRow.TYPE)
rows.add(CustomFieldRow.TYPE)
if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) {
if (entries.isNotEmpty()) {
@ -303,45 +343,4 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
}
}
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.type) {
Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
}

@ -18,7 +18,6 @@ import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
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.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.toCurrency
@ -27,7 +26,7 @@ import java.util.*
import java.util.Currency
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = CustomFieldEntry::class.java

@ -12,9 +12,11 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterItemRow
import net.pokeranalytics.android.ui.view.ImageDecorator
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import timber.log.Timber
import java.util.*
@ -23,7 +25,7 @@ import java.util.*
* It contains a list of [FilterCondition] describing the complete query to launch
* The [Filter] is working closely with a [Filterable] interface providing the entity we want the query being launched on
*/
open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, UsageCountable, ImageDecorator {
open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, CountableUsage, ImageDecorator {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Filter::class.java
@ -35,6 +37,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
val filter = Filter()
filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier
return filter
//return realm.copyToRealm(filter)
}
inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> {
@ -43,6 +46,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
sortField?.let {
realmQuery = realmQuery.sort(it)
}
// val desc = realmQuery.description
return realmQuery.findAll()
}
}
@ -87,25 +91,22 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
return FilterableType.ALL
}
fun createOrUpdateFilterConditions(filterConditionRows: List<FilterItemRow>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.queryCondition?.id }}")
fun createOrUpdateFilterConditions(filterConditionRows: ArrayList<QueryCondition>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.id }}")
Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}")
filterConditionRows
.mapNotNull {
it.queryCondition?.groupId
.map {
it.groupId
}
.distinct()
.forEach { groupId ->
filterConditionRows
.filter {
it.queryCondition?.groupId == groupId
it.groupId == groupId
}
.apply {
val conditions = this.mapNotNull { it.queryCondition }
Timber.d("list of querys: ${conditions.map { it.id }}")
val newFilterCondition = FilterCondition(conditions, this.first().filterSectionRow)
Timber.d("list of querys: ${this.map { it.id }}")
val newFilterCondition = FilterCondition(this)
val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
}
@ -133,19 +134,29 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}")
Timber.d("list of contains ${filterElementRow.id}")
val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first())
Timber.d("is contained: $contained")
Timber.d("list of : $contained")
return contained
}
fun filterCondition(filterElementRow: QueryCondition): FilterCondition? {
return filterConditions.firstOrNull {
it.queryCondition.id.contains(filterElementRow.id.first())
/**
* Get the saved value for the given [filterElementRow]
*/
fun loadValueForElement(filterElementRow: QueryCondition) {
val filtered = filterConditions.filter {
it.queryCondition.id == filterElementRow.id
}
if (filtered.isNotEmpty()) {
return filterElementRow.updateValueBy(filtered.first())
}
}
inline fun <reified T : Filterable> query(firstField: String? = null, vararg remainingFields: String): RealmQuery<T> {
val realmQuery = realm.where<T>()
// if (firstField != null && secondField != null) {
// return this.query.queryWith(realmQuery).distinct(firstField, secondField)
// }
if (firstField != null) {
return this.query.queryWith(realmQuery).distinct(firstField, *remainingFields)
}
@ -194,9 +205,11 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
}
override fun updateValue(value: Any?, row: RowRepresentable) {
val newName = value as String? ?: ""
if (newName.isNotEmpty()) {
name = newName
realm.executeTransaction {
val newName = value as String? ?: ""
if (newName.isNotEmpty()) {
name = newName
}
}
}
}

@ -2,21 +2,19 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import java.util.*
import kotlin.collections.ArrayList
open class FilterCondition() : RealmObject() {
private constructor(filterName: String, sectionName: String) : this() {
private constructor(filterName:String, sectionName:String) : this() {
this.filterName = filterName
this.sectionName = sectionName
}
constructor(filterElementRows: List<QueryCondition>, section: FilterSectionRow) : this(filterElementRows.first().baseId, section.name) {
constructor(filterElementRows: List<QueryCondition>) : this(filterElementRows.first().baseId, filterElementRows.first().filterSectionRow.name) {
val row = filterElementRows.first()
this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName
this.operator = row.operator.ordinal
@ -24,12 +22,11 @@ open class FilterCondition() : RealmObject() {
this.stringValue = row.customFieldId
}
when (row) {
is QueryCondition.SingleInt -> this.setValue(row.singleValue)
is QueryCondition.SingleDate -> this.setValue(row.singleValue)
is QueryCondition.SingleInt -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing)
is QueryCondition.SingleDate -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing)
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.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues })
else -> {}
}
}
@ -51,10 +48,7 @@ open class FilterCondition() : RealmObject() {
var stringValue: String? = null
var operator: Int? = null
@LinkingObjects("filterConditions")
val filters: RealmResults<Filter>? = null
inline fun <reified T> getValues(): ArrayList <T> {
inline fun <reified T:Any > getValues(): ArrayList < T > {
return when (T::class) {
Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } }
Double::class -> ArrayList<T>().apply { doubleValues?.map { add(it as T) } }
@ -63,17 +57,7 @@ open class FilterCondition() : RealmObject() {
}
}
fun <T> getv(clazz: Class<T>) : T {
return when (clazz) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
Date::class -> dateValue ?: Date()
String::class -> stringValue ?: ""
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} as T
}
inline fun <reified T> getValue(): T {
inline fun <reified T:Any > getValue(): T {
return when (T::class) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
@ -107,5 +91,4 @@ open class FilterCondition() : RealmObject() {
fun setValue(value:String) {
stringValue = value
}
}

@ -8,21 +8,20 @@ import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.GamePropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.GameRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable, UsageCountable {
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, CountableUsage {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Game::class.java
@ -72,7 +71,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
): CharSequence {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
GameRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
}
}
@ -80,7 +79,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name))
GamePropertiesRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName))
GameRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName))
else -> null
}
}
@ -88,7 +87,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
GamePropertiesRow.SHORT_NAME -> this.shortName = value as String? ?: ""
GameRow.SHORT_NAME -> this.shortName = value as String? ?: ""
}
}

@ -12,12 +12,11 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import java.util.*
open class Location : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
open class Location : RealmObject(), NameManageable, RowRepresentable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Location::class.java

@ -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,26 @@ import android.content.Context
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
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.RowUpdatable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow
import net.pokeranalytics.android.util.NULL_TEXT
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 kotlin.collections.ArrayList
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable {
open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRowRepresentableDataSource, RowRepresentable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -37,6 +43,13 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
@Ignore
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 {
//TODO
return true
@ -55,16 +68,70 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
return R.string.relationship_error
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
PlayerRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerPropertiesRow.IMAGE -> this.picture = value as? String
PlayerRow.NAME -> this.name = value as String? ?: ""
PlayerRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerRow.IMAGE -> this.picture = value as String? ?: ""
}
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(PlayerRow.IMAGE)
rows.add(PlayerRow.NAME)
rows.add(PlayerRow.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 +141,57 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres
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) {
PlayerRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name))
PlayerRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.summary))
}
return null
}
val initials: String
get() {
return if (this.name.isNotEmpty()) {
@ -94,8 +212,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,6 @@ import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria
@ -31,7 +30,7 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
var name: String = ""
// The type of display of the report
var display: Int = ReportDisplay.TABLE.ordinal
var display: Int = Calculator.Options.Display.TABLE.ordinal
/**
* A list of statIds to compute
@ -65,30 +64,30 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
/**
* Returns the Options based on the ReportSetup parameters
*/
fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options {
fun options(realm: Realm): Calculator.Options {
val stats = this.statIds.map { Stat.valueByIdentifier(it) }
val stats = this.statIds.map { Stat.valueByIdentifier(it) }
// Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
// Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
val cfCriteria = customFields.map { it.criteria }
val cfCriteria = customFields.map { it.criteria }
val allCriteria = mutableListOf<Criteria>()
allCriteria.addAll(criteria)
allCriteria.addAll(cfCriteria)
val allCriteria = mutableListOf<Criteria>()
allCriteria.addAll(criteria)
allCriteria.addAll(cfCriteria)
return Calculator.Options(
return Calculator.Options(
display = Calculator.Options.Display.values()[this.display],
stats = stats,
progressValues = reportDisplay.progressValues,
criterias = allCriteria,
filter = this.filter,
userGenerated = true,
reportSetupId = this.id
)
}
)
}
// Deletable

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

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

@ -8,21 +8,21 @@ import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentFeatureRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentFeatureRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, UsageCountable {
open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable,
CountableUsage {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {

@ -13,16 +13,14 @@ import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentNameRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentNameRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource,
RowUpdatable, RowRepresentable {
open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()

@ -1,6 +1,7 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import com.github.mikephil.charting.data.Entry
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
@ -11,21 +12,25 @@ import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.TransactionPropertiesRow
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionRow
import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.extensions.findById
import java.text.DateFormat
import java.util.*
import kotlin.math.abs
import kotlin.collections.ArrayList
open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageable, StaticRowRepresentableDataSource, TimeFilterable,
open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, TimeFilterable,
Filterable, DatedBankrollGraphEntry {
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())
transaction.date = date ?: Date()
@ -33,16 +38,16 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
transaction.type = type
transaction.bankroll = bankroll
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
}
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(TransactionRow.values())
rows
}
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
return when (queryCondition) {
@ -73,13 +78,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
// The amount of the transaction
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
override var date: Date = Date()
@ -94,12 +92,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
// A user comment
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
override var dayOfWeek: Int? = null
override var month: Int? = null
@ -109,34 +101,18 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
@Ignore
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) {
when (row) {
TransactionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
TransactionPropertiesRow.TYPE -> type = value as TransactionType?
TransactionPropertiesRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionPropertiesRow.COMMENT -> comment = value as String? ?: ""
TransactionPropertiesRow.DATE -> date = value as Date? ?: Date()
TransactionPropertiesRow.DESTINATION -> destination = value as? Bankroll
TransactionPropertiesRow.RATE -> transferRate = (value as String?)?.toDouble()
TransactionRow.BANKROLL -> bankroll = value as Bankroll?
TransactionRow.TYPE -> type = value as TransactionType?
TransactionRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionRow.COMMENT -> comment = value as String? ?: ""
TransactionRow.DATE -> date = value as Date? ?: Date()
}
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation()
return rowRepresentation
}
override fun isValidForSave(): Boolean {
@ -172,24 +148,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
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
@Ignore
@ -205,14 +163,14 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
override fun legendValues(
stat: Stat,
total: Double,
style: Graph.Style,
entry: Entry,
style: GraphFragment.Style,
groupName: String,
context: Context
): LegendContent {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(total, currency = null)
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null)
val leftName = context.getString(R.string.amount)
return DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName)
}

@ -13,16 +13,16 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.Localizable
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.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TransactionTypePropertiesRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionTypeRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import java.util.*
import kotlin.collections.ArrayList
open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource,
UsageCountable {
open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable,
CountableUsage {
enum class Value(override var uniqueIdentifier: Int, val additive: Boolean) : IntIdentifiable, Localizable {
@ -30,9 +30,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
DEPOSIT(1, true),
BONUS(2, true),
STACKING_INCOMING(3, true),
STACKING_OUTGOING(4, false),
TRANSFER(5, false),
EXPENSE(6, false); // not created by default, only used for poker base import atm
STACKING_OUTGOING(4, false);
companion object : IntSearchable<Value> {
@ -49,8 +47,6 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
BONUS -> R.string.bonus
STACKING_INCOMING -> R.string.stacking_incoming
STACKING_OUTGOING -> R.string.stacking_outgoing
TRANSFER -> R.string.transfer
EXPENSE -> R.string.expense
}
}
@ -60,7 +56,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(TransactionTypePropertiesRow.values())
rows.addAll(TransactionTypeRow.values())
rows
}
@ -72,10 +68,6 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
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 {
val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst()
return if (type != null) {
@ -139,7 +131,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
override fun boolForRow(row: RowRepresentable): Boolean {
return when (row) {
TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive
TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive
else -> super.boolForRow(row)
}
}
@ -151,7 +143,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false
TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false
}
}

@ -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.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import timber.log.Timber
interface CardProperty
@ -25,10 +24,11 @@ fun List<Card>.formatted(context: Context) : CharSequence? {
var span: CharSequence? = null
this.forEach {
val formatted = it.formatted(context)
span = if (span == null) {
formatted
} else {
TextUtils.concat(span, formatted)
if (span == null) {
span = formatted
}
else {
span = TextUtils.concat(span, formatted)
}
}
return span
@ -95,10 +95,6 @@ open class Card : RealmObject() {
companion object {
val baseSuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER)
}
val displaySuits: List<Suit> by lazy {
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
get() {
return this.value == null || this.isSuitWildCard
}
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 this.value == null || this.suit == null || this.suit == Suit.UNDEFINED
}
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.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.interfaces.Deletable
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.ui.modules.handhistory.evaluator.EvaluatorBridge
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.formatted
import net.pokeranalytics.android.util.extensions.fullDate
import timber.log.Timber
import java.util.*
import kotlin.math.max
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean)
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable,
CardHolder, Comparator<PositionAmount>, StakesHolder {
CardHolder, Comparator<PositionAmount> {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -62,67 +63,34 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/***
* The small blind
*/
var oldSmallBlind: Double? = null
var smallBlind: Double? = null
set(value) {
field = value
// if (this.bigBlind == null && value != null) {
// this.bigBlind = value * 2
// }
if (this.bigBlind == null && value != null) {
this.bigBlind = value * 2
}
}
/***
* The big blind
*/
var oldBigBlind: Double? = null
var bigBlind: Double? = null
set(value) {
field = value
// if (this.smallBlind == null && value != null) {
// this.smallBlind = value / 2
// }
if (this.smallBlind == null && value != null) {
this.smallBlind = value / 2
}
}
/***
* Big blind ante
*/
var bigBlindAnte: Boolean = false
/***
* The ante
*/
override 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
var ante: Double = 0.0
/***
* The coded stakes
* Big blind ante
*/
override var stakes: String? = null
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
}
var bigBlindAnte: Boolean = false
/***
* Number of players in the hand
@ -190,15 +158,13 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/***
* 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.ante?.let { this.ante = it }
handSetup.blinds?.let { this.blinds = it }
handSetup.smallBlind?.let { this.smallBlind = it }
handSetup.bigBlind?.let { this.bigBlind = it }
this.session = handSetup.session
this.date = this.session?.handHistoryAutomaticDate ?: Date()
@ -213,32 +179,18 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.actions.clear()
var blindValues = this.blindValues
if (blindValues.isNotEmpty()) {
blindValues.forEachIndexed { index, blind ->
val action = when(index) {
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)
}
this.addAction(0, Action.Type.POST_SB, this.smallBlind)
this.addAction(1, Action.Type.POST_BB, this.bigBlind)
// var lastStraddler: Int? = null
blindValues = blindValues.drop(2)
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 amount = if (index < blindValues.size) { blindValues[index] } else null
this.addAction(positionIndex, Action.Type.STRADDLE, amount)
this.addAction(positionIndex, Action.Type.STRADDLE)
// lastStraddler = positionIndex
}
// var lastStraddler: Int? = null
// val totalActions = this.actions.size
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions
@ -284,9 +236,9 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val anteSum: Double
get() {
return if (bigBlindAnte) {
this.biggestBet ?: 0.0
this.bigBlind ?: 0.0
} 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 firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index
?: sortedActions.size
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumOf { it.effectiveAmount }
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumByDouble { it.effectiveAmount }
}
@Ignore
@ -350,20 +302,14 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}"
val firstLineComponents = mutableListOf(this.date.fullDate(), players)
this.blinds?.let { firstLineComponents.add(it) }
// this.smallBlind?.let { sb ->
// this.bigBlind?.let { bb ->
// firstLineComponents.add("${sb.formatted}/${bb.formatted}")
// }
// }
this.ante?.let {
if (it > 0.0) {
firstLineComponents.add("ante ${this.ante}")
this.smallBlind?.let { sb ->
this.bigBlind?.let { bb ->
firstLineComponents.add("${sb.formatted}/${bb.formatted}")
}
}
if (this.ante > 0.0) {
firstLineComponents.add("ante ${this.ante}")
}
string = string.plus(firstLineComponents.joinToString(" - "))
string = string.addLineReturn(2)
@ -382,26 +328,26 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
// Actions per street
val sortedActions = this.actions.sortedBy { it.index }
// Remove SUMMARY
Street.values().dropLast(1).forEach { street ->
// val actionList = ActionList()
// actionList.load(this)
Street.values().forEach { street ->
string = string.addLineReturn(2)
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex)
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()) {
if (streetActions.isNotEmpty()) {
val streetItems = mutableListOf<CharSequence>(context.getString(street.resId))
val potSize = this.potSizeForStreet(street)
if (potSize > 0) {
// streetItems.add(context.getString(R.string.pot_size))
streetItems.add("(" + potSize.formatted + ")")
}
string = string.plus(streetItems.joinToString(" "))
val streetCards = this.cardsForStreet(street)
if (streetCards.isNotEmpty()) {
string = string.addLineReturn()
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.addLineReturn()
}
}
}
@ -426,12 +371,12 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
fun anteForPosition(position: Position): Double {
return if (this.bigBlindAnte) {
if (position == Position.BB) {
this.biggestBet ?: 0.0
this.bigBlind ?: 0.0
} else {
0.0
}
} 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)
playerItems.add("- $heroString")
}
if (playerSetup.cards.isNotEmpty()) {
playerItems.add("[${playerSetup.cards.formatted(context)}]")
}
playerItems.add("[${playerSetup.cards.formatted(context)}]")
playerSetup.stack?.let { stack ->
playerItems.add("- $stack")
}
@ -479,12 +420,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean?
get() {
return this.heroIndex?.let { heroIndex ->
this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
this.winnerPots.any { it.position == heroIndex }
} ?: run {
null
}
@ -568,8 +504,6 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.winnerPots.clear()
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 }
if (allinPositions.isEmpty()) {
runningPotAmount += streetActions.sumOf { it.effectiveAmount }
runningPotAmount += streetActions.sumByDouble { it.effectiveAmount }
} else {
val amountsPerPosition = mutableListOf<PositionAmount>()
// get all committed amounts for the street by player, by allin
this.positionIndexes.map { 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.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 ->
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
}
@ -675,32 +606,17 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
*/
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
val results = noWildCardHands.map { cards ->
cards?.let { hand ->
EvaluatorBridge.playerHand(hand, noWildCardBoard)
val results = activePositions.map {
this.playerSetupForPosition(it)?.cards?.let { hand ->
EvaluatorBridge.playerHand(hand, this.board)
} ?: run {
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)
return results.minOrNull()?.let { best ->
return results.min()?.let { best ->
val winners = mutableListOf<Int>()
results.forEachIndexed { index, i ->
if (i == best && i != Int.MAX_VALUE) {
@ -743,33 +659,4 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
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
}
}
}

@ -1,14 +0,0 @@
package net.pokeranalytics.android.model.realm.rows
import android.content.Context
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.ui.view.RowRepresentable
class BankrollRow(var bankroll: Bankroll) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return this.bankroll.name
}
}

@ -1,57 +0,0 @@
package net.pokeranalytics.android.model.realm.rows
import android.content.Context
import android.text.InputType
import io.realm.RealmList
import io.realm.annotations.Ignore
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
class CustomFieldRow(var customField: CustomField) : RowRepresentable {
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.customField.name
}
override fun getDisplayName(context: Context): String {
return this.customField.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.customField.type) {
CustomField.Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.customField.type) {
CustomField.Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
}

@ -0,0 +1,11 @@
package net.pokeranalytics.android.model.retrofit
import com.google.gson.annotations.SerializedName
/**
* CurrencyCode Converter mapping class
*/
class CurrencyConverterValue {
@SerializedName("val")
var value: Double? = 0.0
}

@ -11,18 +11,15 @@ class DataUtils {
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 {
var sessionQuery = realm.where(Session::class.java)
fun sessionCount(realm: Realm, startDate: Date, endDate: Date, net: Double): Int {
val sessions = realm.where(Session::class.java)
.equalTo("startDate", startDate)
.equalTo("endDate", endDate)
.equalTo("result.net", net)
endDate?.let {
sessionQuery = sessionQuery.equalTo("endDate", it)
}
return sessionQuery.findAll().size
.findAll()
return sessions.size
}
/**

@ -5,7 +5,7 @@ import io.realm.Realm
import io.realm.Sort
import net.pokeranalytics.android.model.realm.Location
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow
/**
* Returns all significant parameters concatenated in a String
@ -24,24 +24,24 @@ private fun Session.parameterRepresentation(context: Context): String {
/**
* Returns a list of fields used to determine which kind of session is favorite
*/
private fun Session.significantFields(): List<SessionPropertiesRow> {
private fun Session.significantFields(): List<SessionRow> {
when (this.type) {
Session.Type.TOURNAMENT.ordinal -> {
return listOf(
SessionPropertiesRow.GAME,
SessionPropertiesRow.INITIAL_BUY_IN,
SessionPropertiesRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE,
SessionPropertiesRow.TOURNAMENT_NAME,
SessionPropertiesRow.TOURNAMENT_TYPE
SessionRow.GAME,
SessionRow.INITIAL_BUY_IN,
SessionRow.BANKROLL,
SessionRow.TABLE_SIZE,
SessionRow.TOURNAMENT_NAME,
SessionRow.TOURNAMENT_TYPE
)
}
Session.Type.CASH_GAME.ordinal -> {
return listOf(
SessionPropertiesRow.GAME,
SessionPropertiesRow.STAKES,
SessionPropertiesRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE
SessionRow.GAME,
SessionRow.BLINDS,
SessionRow.BANKROLL,
SessionRow.TABLE_SIZE
)
}
}
@ -75,7 +75,8 @@ class FavoriteSessionFinder {
*/
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 ->
@ -86,8 +87,8 @@ class FavoriteSessionFinder {
when (session.type) {
Session.Type.CASH_GAME.ordinal -> {
session.cgAnte = fav.cgAnte
session.cgBlinds = fav.cgBlinds
session.cgSmallBlind = fav.cgSmallBlind
session.cgBigBlind = fav.cgBigBlind
}
Session.Type.TOURNAMENT.ordinal -> {
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) {
values.forEach { value ->
if (value != TransactionType.Value.EXPENSE) {
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
}
}
}

@ -8,8 +8,8 @@ import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.activity_color_picker.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.ActivityColorPickerBinding
import net.pokeranalytics.android.ui.activity.components.BaseActivity
class ColorPickerActivity : BaseActivity() {
@ -33,17 +33,15 @@ class ColorPickerActivity : BaseActivity() {
}
private lateinit var binding: ActivityColorPickerBinding
override fun onCreate(savedInstanceState: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // used to fix Oreo crash
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
binding = ActivityColorPickerBinding.inflate(layoutInflater)
setContentView(binding.root)
this.title = getString(R.string.select_a_color)
setContentView(R.layout.activity_color_picker)
initUI()
}
@ -51,29 +49,29 @@ class ColorPickerActivity : BaseActivity() {
* Init UI
*/
private fun initUI() {
binding.color1.setOnClickListener { manageSelectedColor(it) }
binding.color2.setOnClickListener { manageSelectedColor(it) }
binding.color3.setOnClickListener { manageSelectedColor(it) }
binding.color4.setOnClickListener { manageSelectedColor(it) }
binding.color5.setOnClickListener { manageSelectedColor(it) }
binding.color6.setOnClickListener { manageSelectedColor(it) }
binding.color7.setOnClickListener { manageSelectedColor(it) }
binding.color8.setOnClickListener { manageSelectedColor(it) }
binding.color9.setOnClickListener { manageSelectedColor(it) }
color1.setOnClickListener { manageSelectedColor(it) }
color2.setOnClickListener { manageSelectedColor(it) }
color3.setOnClickListener { manageSelectedColor(it) }
color4.setOnClickListener { manageSelectedColor(it) }
color5.setOnClickListener { manageSelectedColor(it) }
color6.setOnClickListener { manageSelectedColor(it) }
color7.setOnClickListener { manageSelectedColor(it) }
color8.setOnClickListener { manageSelectedColor(it) }
color9.setOnClickListener { manageSelectedColor(it) }
}
private fun manageSelectedColor(view: View) {
val color = when(view) {
binding.color1 -> getColor(R.color.player_color_1)
binding.color2 -> getColor(R.color.player_color_2)
binding.color3 -> getColor(R.color.player_color_3)
binding.color4 -> getColor(R.color.player_color_4)
binding.color5 -> getColor(R.color.player_color_5)
binding.color6 -> getColor(R.color.player_color_6)
binding.color7 -> getColor(R.color.player_color_7)
binding.color8 -> getColor(R.color.player_color_8)
binding.color9 -> getColor(R.color.player_color_9)
color1 -> getColor(R.color.player_color_1)
color2 -> getColor(R.color.player_color_2)
color3 -> getColor(R.color.player_color_3)
color4 -> getColor(R.color.player_color_4)
color5 -> getColor(R.color.player_color_5)
color6 -> getColor(R.color.player_color_6)
color7 -> getColor(R.color.player_color_7)
color8 -> getColor(R.color.player_color_8)
color9 -> getColor(R.color.player_color_9)
else -> getColor(R.color.player_color_1)
}

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

@ -3,13 +3,12 @@ package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import net.pokeranalytics.android.databinding.ActivityGdprBinding
import kotlinx.android.synthetic.main.activity_gdpr.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.BaseActivity
class GDPRActivity : BaseActivity() {
private lateinit var binding: ActivityGdprBinding
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, GDPRActivity::class.java)
@ -19,8 +18,7 @@ class GDPRActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGdprBinding.inflate(layoutInflater)
setContentView(binding.root)
setContentView(R.layout.activity_gdpr)
initUI()
}
@ -29,8 +27,10 @@ class GDPRActivity : BaseActivity() {
* Init UI
*/
private fun initUI() {
setSupportActionBar(binding.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}

@ -3,68 +3,53 @@ package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet
import kotlinx.android.synthetic.main.activity_graph.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.ActivityGraphBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.viewmodel.GraphViewModel
import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder
class GraphActivity : BaseActivity(), ViewModelHolder {
override val model: GraphViewModel by lazy {
ViewModelProvider(this).get(GraphViewModel::class.java)
override val viewModel: GraphViewModel by lazy {
ViewModelProviders.of(this).get(GraphViewModel::class.java)
}
companion object {
const val styleKey = "style"
private var lineDataSets: List<LineDataSet>? = null
private var barDataSets: List<BarDataSet>? = null
// private var style: Graph.Style? = null
private var style: GraphFragment.Style = GraphFragment.Style.LINE
private var activityTitle: String? = null
/***
* Line graph constructor
/**
* Default constructor
*/
fun newLineInstance(context: Context, lineDataSets: List<LineDataSet>, title: String? = null) {
fun newInstance(
context: Context, lineDataSets: List<LineDataSet>? = null,
barDataSets: List<BarDataSet>? = null,
style: GraphFragment.Style = GraphFragment.Style.LINE,
title: String? = null
) {
this.lineDataSets = lineDataSets
// this.style = Graph.Style.LINE
this.activityTitle = title
val intent = Intent(context, GraphActivity::class.java)
intent.putExtra(styleKey, Graph.Style.LINE.ordinal)
context.startActivity(intent)
}
/***
* Bar graph constructor
*/
fun newBarInstance(context: Context, barDataSets: List<BarDataSet>, title: String? = null) {
this.barDataSets = barDataSets
// this.style = Graph.Style.BAR
this.style = style
this.activityTitle = title
val intent = Intent(context, GraphActivity::class.java)
intent.putExtra(styleKey, Graph.Style.BAR.ordinal)
context.startActivity(intent)
}
}
private lateinit var binding: ActivityGraphBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGraphBinding.inflate(layoutInflater)
setContentView(binding.root)
setContentView(R.layout.activity_graph)
initUI()
}
@ -73,10 +58,10 @@ class GraphActivity : BaseActivity(), ViewModelHolder {
*/
private fun initUI() {
this.model.title = activityTitle
this.viewModel.title = activityTitle
this.model.title?.let {
setSupportActionBar(binding.toolbar)
this.viewModel.title?.let {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = activityTitle
activityTitle = null
@ -84,28 +69,19 @@ class GraphActivity : BaseActivity(), ViewModelHolder {
barDataSets?.let {
if (it.size == 1) {
this.model.setBarDataSet(it.first())
this.viewModel.setBarDataSet(it.first())
} else {
throw PAIllegalStateException("Unexpected number of bar data sets")
}
}
lineDataSets?.let {
if (it.size == 1) {
this.model.setLineDataSet(it.first())
this.viewModel.setLineDataSet(it.first())
} else {
throw PAIllegalStateException("Unexpected number of line data sets")
}
}
val styleIndex = this.intent.getIntExtra(styleKey, -1)
val style = Graph.Style.values()[styleIndex]
// style?.let {
// this.model.style = it
// }
//
// val style = style ?: this.model.style ?: throw PAIllegalStateException("Graph style not defined")
val fragmentTransaction = supportFragmentManager.beginTransaction()
val graphFragment = GraphFragment.newInstance(style)
fragmentTransaction.add(R.id.container, graphFragment)

@ -7,33 +7,16 @@ import android.os.Build
import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults
import kotlinx.android.synthetic.main.activity_home.*
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.NewPerformanceListener
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.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity
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.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 {
fun newInstance(context: Context, id: Int) {
@ -47,29 +30,23 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
private var homePagerAdapter: HomePagerAdapter? = null
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (binding.viewPager.currentItem == Tab.REPORTS.identifier) {
this.paApplication.reportWhistleBlower?.clearNotifications()
}
when (item.itemId) {
R.id.navigation_history -> {
displayFragment(Tab.HISTORY)
displayFragment(0)
}
R.id.navigation_stats -> {
displayFragment(Tab.STATS)
displayFragment(1)
}
R.id.navigation_calendar -> {
displayFragment(Tab.CALENDAR)
displayFragment(2)
}
R.id.navigation_reports -> {
displayFragment(Tab.REPORTS)
displayFragment(3)
}
R.id.navigation_settings -> {
displayFragment(Tab.SETTINGS)
displayFragment(4)
}
}
binding.navigation.getOrCreateBadge(item.itemId).isVisible = false
return@OnNavigationItemSelectedListener true
}
@ -77,18 +54,11 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
super.onResume()
AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed()
lookForCalendarBadge()
checkForFailedBackups()
}
private lateinit var binding: ActivityHomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
if (BuildConfig.DEBUG) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
@ -98,18 +68,58 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
}
}
setContentView(R.layout.activity_home)
observeRealmObjects()
initUI()
checkFirstLaunch()
this.paApplication.reportWhistleBlower?.addListener(this)
}
override fun onDestroy() {
super.onDestroy()
this.paApplication.reportWhistleBlower?.removeListener(this)
}
// override fun onNewIntent(intent: Intent?) {
// super.onNewIntent(intent)
//
// 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() {
@ -117,9 +127,9 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
// observe currency changes
this.currencies = realm.where(Currency::class.java).findAll()
this.currencies.addChangeListener { currencies, _ ->
this.currencies.addChangeListener { t, _ ->
realm.executeTransaction {
currencies.forEach {
t.forEach {
it.refreshRelatedRatedValues()
}
}
@ -131,16 +141,13 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
*/
private fun initUI() {
binding.navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
binding.navigation.selectedItemId = R.id.navigation_history
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.selectedItemId = R.id.navigation_history
homePagerAdapter = HomePagerAdapter(supportFragmentManager)
val viewPager = binding.viewPager
viewPager.offscreenPageLimit = 5
viewPager.enablePaging = false
viewPager.adapter = homePagerAdapter
}
/**
@ -160,70 +167,8 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
/**
* Display a new fragment
*/
private fun displayFragment(tab: Tab) {
binding.viewPager.setCurrentItem(tab.identifier, 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()
}
}
private fun displayFragment(index: Int) {
viewPager.setCurrentItem(index, false)
}
}

@ -9,7 +9,6 @@ import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity
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.util.billing.AppGuard
import net.pokeranalytics.android.util.extensions.count
import timber.log.Timber
class ImportActivity : BaseActivity() {
@ -49,7 +49,7 @@ class ImportActivity : BaseActivity() {
intent?.data?.let {
this.fileURI = it
} ?: 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)
@ -63,13 +63,11 @@ class ImportActivity : BaseActivity() {
val fragmentTransaction = supportFragmentManager.beginTransaction()
val fragment = ImportFragment()
fragment.setData(fileURI)
// val fis = contentResolver.openInputStream(fileURI)
// Timber.d("Load fragment data with: $fis")
// fis?.let {
// fragment.setData(it)
// }
val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis")
fis?.let {
fragment.setData(it)
}
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
@ -84,7 +82,7 @@ class ImportActivity : BaseActivity() {
when (requestCode) {
RequestCode.IMPORT.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()
})
}

@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.activity.components.ReportActivity
@ -21,7 +20,7 @@ class ProgressReportActivity : ReportActivity() {
*/
fun newInstance(context: Context, report: Report, title: String, stat: Stat? = null, displayAggregationChoices: Boolean = true) {
val parameters = ReportParameters(report, ReportDisplay.PROGRESS, title, stat, showAggregationChoices = displayAggregationChoices)
val parameters = ReportParameters(report, title, stat, showAggregationChoices = displayAggregationChoices)
ReportViewModel.defineParameters(parameters)
val intent = Intent(context, ProgressReportActivity::class.java)
context.startActivity(intent)

@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -15,7 +14,6 @@ class ReportCreationActivity : BaseActivity() {
companion object {
var options: Calculator.Options? = null
var reportDisplay: ReportDisplay? = null
fun newInstanceForResult(fragment: Fragment, context: Context) {
val intent = Intent(context, ReportCreationActivity::class.java)

@ -3,7 +3,6 @@ package net.pokeranalytics.android.ui.activity.components
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.view.MenuItem
@ -11,24 +10,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.crashlytics.android.Crashlytics
import com.google.android.libraries.places.api.model.PlaceLikelihood
import io.realm.Realm
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.model.realm.Location
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.util.CrashLogging
import net.pokeranalytics.android.util.LocationManager
import net.pokeranalytics.android.util.PermissionRequest
import net.pokeranalytics.android.util.Preferences
import java.util.*
class RootBottomSheetViewModel: ViewModel() {
var rowRepresentable: RowRepresentable? = null
var delegate: RowRepresentableDelegate? = null
}
abstract class BaseActivity : AppCompatActivity() {
@ -41,71 +30,55 @@ abstract class BaseActivity : AppCompatActivity() {
private var permissionRequest: PermissionRequest? = null
val paApplication: PokerAnalyticsApplication
get() { return this.application as PokerAnalyticsApplication }
val bottomSheetViewModel: RootBottomSheetViewModel by lazy {
ViewModelProvider(this).get(RootBottomSheetViewModel::class.java)
}
fun setBottomSheetParameters(rowRepresentable: RowRepresentable, delegate: RowRepresentableDelegate) {
this.bottomSheetViewModel.rowRepresentable = rowRepresentable
this.bottomSheetViewModel.delegate = delegate
}
// Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState")
setLanguage()
Crashlytics.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState")
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState")
Crashlytics.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState")
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash
setLanguage()
}
override fun onResume() {
super.onResume()
CrashLogging.log("$this.localClassName onResume")
Crashlytics.log("$this.localClassName onResume")
}
override fun onPause() {
super.onPause()
CrashLogging.log("$this.localClassName onPause")
Crashlytics.log("$this.localClassName onPause")
}
override fun onStop() {
super.onStop()
CrashLogging.log("$this.localClassName onStop")
Crashlytics.log("$this.localClassName onStop")
}
override fun onDestroy() {
super.onDestroy()
CrashLogging.log("$this.localClassName onDestroy")
Crashlytics.log("$this.localClassName onDestroy")
this.realm?.close()
}
override fun onBackPressed() {
super.onBackPressed()
AppReviewManager.showReviewManager(this)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_ACCESS_FINE_LOCATION -> {
if (permissions.isNotEmpty() && permissions[0] == ACCESS_FINE_LOCATION
&& grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
&& grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionCallback?.invoke(true)
} else {
permissionCallback?.invoke(false)
// permission denied, boo! Disable the
// functionality that depends on this permission.
// showMessage(getString(R.string.error));
}
this.permissionCallback = null
}
}
@ -115,15 +88,16 @@ abstract class BaseActivity : AppCompatActivity() {
val allPermissionsPresent = permissions.all { request.permissions.contains(it) }
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
request.callback.invoke(allPermissionsPresent && allGranted)
this.permissionRequest = null
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
item?.let {
if (it.itemId == android.R.id.home) {
onBackPressed()
}
}
return super.onOptionsItemSelected(item)
}
@ -134,27 +108,6 @@ abstract class BaseActivity : AppCompatActivity() {
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
*/
@ -175,14 +128,6 @@ abstract class BaseActivity : AppCompatActivity() {
return ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
/**
* Ask for app permission
*/
fun askForPermission(permissions: Array<String>, requestCode: Int, permissionCallback: ((granted: Boolean) -> Unit)) {
this.permissionRequest = PermissionRequest(permissions, permissionCallback, requestCode)
ActivityCompat.requestPermissions(this, permissions, requestCode)
}
/**
* Ask for location permission
*/
@ -193,6 +138,24 @@ abstract class BaseActivity : AppCompatActivity() {
)
}
/**
* Ask for places request
*/
fun askForPlacesRequest(callback: ((success: Boolean, places: ArrayList<PlaceLikelihood>) -> Unit)?) {
// Call findCurrentPlace and handle the response (first check that the user has granted permission).
if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
LocationManager(this).askForPlacesRequest(callback)
} else {
askForLocationPermission { granted ->
if (granted) {
LocationManager(this).askForPlacesRequest(callback)
} else {
callback?.invoke(false, ArrayList())
}
}
}
}
/**
* Find the nearest location from the user
*/
@ -214,6 +177,14 @@ abstract class BaseActivity : AppCompatActivity() {
}
}
/**
* Ask for app permission
*/
fun askForPermission(permissions: Array<String>, requestCode: Int, permissionCallback: ((granted: Boolean) -> Unit)) {
this.permissionRequest = PermissionRequest(permissions, permissionCallback, requestCode)
ActivityCompat.requestPermissions(this, permissions, requestCode)
}
/**
* Find the current location
*/

@ -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),
SUBSCRIPTION(901),
CURRENCY(902),
PERMISSION_WRITE_EXTERNAL_STORAGE(1000),
CAMERA(1001)
PERMISSION_WRITE_EXTERNAL_STORAGE(1000)
}
enum class ResultCode(var value: Int) {

@ -4,7 +4,6 @@ import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.provider.MediaStore
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@ -137,7 +136,6 @@ open class MediaActivity : BaseActivity() {
selectedChoice = -1
}
// Build.VERSION.SDK_INT < Build.VERSION_CODES.R &&
/**
* Open the Camera Intent
@ -149,40 +147,35 @@ open class MediaActivity : BaseActivity() {
this.mCurrentPhotoPath = null
this.multiplePictures = multiplePictures
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 200) { granted ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || granted) {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// 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
}
// 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
}
// 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)
}
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(packageManager) != null) {
// 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
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
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 201) { granted ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || granted) {
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)
}
// Test if we have the permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
selectedChoice = SELECTED_CHOICE_SELECT_PICTURE
askForStoragePermission()
return
}
this.multiplePictures = multiplePictures
// Test if we have the permission
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// selectedChoice = SELECTED_CHOICE_SELECT_PICTURE
// askForStoragePermission()
// return
// }
//
// 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)
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

@ -4,19 +4,27 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import androidx.lifecycle.ViewModelProviders
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.viewmodel.ReportViewModel
import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder
class ReportParameters(var report: Report, var reportDisplay: ReportDisplay, var title: String, var stat: Stat? = null, var showAggregationChoices: Boolean = true)
class ReportParameters(var report: Report, var title: String, var stat: Stat? = null, var showAggregationChoices: Boolean = true)
abstract class ReportActivity : BaseActivity(), ViewModelHolder {
override val model: ReportViewModel by lazy {
ViewModelProvider(this).get(ReportViewModel::class.java)
// private val viewModelFactory: ReportViewModelFactory by lazy {
// ReportViewModel.parameters?.let {
// val stat = it.stat ?: it.report.options.stats.first()
// ReportViewModelFactory(it.report, stat, it.title, it.showAggregationChoices)
// } ?: run {
// throw PAIllegalStateException("null report parameters")
// }
// }
override val viewModel: ReportViewModel by lazy {
ViewModelProviders.of(this).get(ReportViewModel::class.java)
}
companion object {
@ -24,19 +32,19 @@ abstract class ReportActivity : BaseActivity(), ViewModelHolder {
/**
* Default constructor
*/
fun newInstance(context: Context, report: Report, reportDisplay: ReportDisplay, reportTitle: String, stat: Stat? = null) {
// val options = report.options
val parameters = ReportParameters(report, reportDisplay, reportTitle, stat)
fun newInstance(context: Context, report: Report, reportTitle: String, stat: Stat? = null) {
val options = report.options
val parameters = ReportParameters(report, reportTitle, stat)
ReportViewModel.defineParameters(parameters)
val intent = Intent(context, reportDisplay.activityClass)
val intent = Intent(context, options.display.activityClass)
context.startActivity(intent)
}
fun newInstanceForResult(fragment: Fragment, report: Report, reportDisplay: ReportDisplay, reportTitle: String, stat: Stat? = null) {
// val options = report.options
val parameters = ReportParameters(report, reportDisplay, reportTitle, stat)
fun newInstanceForResult(fragment: Fragment, report: Report, reportTitle: String, stat: Stat? = null) {
val options = report.options
val parameters = ReportParameters(report, reportTitle, stat)
ReportViewModel.defineParameters(parameters)
val intent = Intent(fragment.requireContext(), reportDisplay.activityClass)
val intent = Intent(fragment.requireContext(), options.display.activityClass)
fragment.startActivityForResult(intent, RequestCode.DEFAULT.value)
}
@ -50,12 +58,11 @@ abstract class ReportActivity : BaseActivity(), ViewModelHolder {
private fun initViewModelWithParameters() {
ReportViewModel.parameters?.let {
this.model.report = it.report
this.model.reportDisplay = it.reportDisplay
this.model.title = it.title
this.viewModel.report = it.report
this.viewModel.title = it.title
val stat = it.stat ?: it.report.options.stats.first()
this.model.stat = stat
this.model.showAggregationChoices = it.showAggregationChoices
this.viewModel.stat = stat
this.viewModel.showAggregationChoices = it.showAggregationChoices
// Crashlytics.log("initViewModelWithParameters, stat = ${stat.uniqueIdentifier}")

@ -6,11 +6,10 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment
import net.pokeranalytics.android.ui.modules.feed.FeedFragment
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment
import java.lang.ref.WeakReference
/**
@ -22,10 +21,10 @@ class ComparisonChartPagerAdapter(val context: Context, fragmentManager: Fragmen
override fun getItem(position: Int): BaseFragment {
return when (position) {
0 -> GraphFragment.newInstance(Graph.Style.BAR)
1 -> GraphFragment.newInstance(Graph.Style.MULTILINE)
0 -> GraphFragment.newInstance(GraphFragment.Style.BAR)
1 -> GraphFragment.newInstance(GraphFragment.Style.MULTILINE)
2 -> CalendarFragment.newInstance()
else -> throw PAIllegalStateException("There should not be more than $count items, position = $position")
else -> FeedFragment.newInstance()
}
}

@ -2,17 +2,13 @@ package net.pokeranalytics.android.ui.adapter
import android.util.SparseArray
import android.view.ViewGroup
import androidx.core.util.forEach
import androidx.core.util.size
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
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.SettingsFragment
import net.pokeranalytics.android.ui.fragment.StatisticsFragment
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 java.lang.ref.WeakReference
@ -20,23 +16,23 @@ import java.lang.ref.WeakReference
* Home Adapter
*/
class HomePagerAdapter(fragmentManager: FragmentManager) :
FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private var weakReferences = SparseArray<WeakReference<BaseFragment>>()
override fun getItem(position: Int): BaseFragment {
return when (position) {
Tab.HISTORY.identifier -> FeedFragment.newInstance()
Tab.STATS.identifier -> StatisticsFragment.newInstance()
Tab.CALENDAR.identifier -> CalendarFragment.newInstance()
Tab.REPORTS.identifier -> ReportsFragment.newInstance()
Tab.SETTINGS.identifier -> SettingsFragment.newInstance()
else -> throw PAIllegalStateException("Should not happen, position = $position")
0 -> FeedFragment.newInstance()
1 -> StatisticsFragment.newInstance()
2 -> CalendarFragment.newInstance()
3 -> ReportsFragment.newInstance()
4 -> SettingsFragment.newInstance()
else -> FeedFragment.newInstance()
}
}
override fun getCount(): Int {
return Tab.values().size
return 5
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
@ -52,27 +48,30 @@ class HomePagerAdapter(fragmentManager: FragmentManager) :
override fun getItemPosition(obj: Any): Int {
return when (obj) {
is FeedFragment -> Tab.HISTORY.identifier
is StatisticsFragment -> Tab.STATS.identifier
is CalendarFragment -> Tab.CALENDAR.identifier
is ReportsFragment -> Tab.REPORTS.identifier
is SettingsFragment -> Tab.SETTINGS.identifier
else -> throw PAIllegalStateException("Should not happen for object $obj")
FeedFragment::class.java -> 0
StatisticsFragment::class.java -> 1
CalendarFragment::class.java -> 2
ReportsFragment::class.java -> 3
SettingsFragment::class.java -> 4
else -> -1
}
}
fun activityResumed() {
this.weakReferences.forEach { _, value ->
value.get()?.activityResumed()
val ref = this.weakReferences.get(0)
ref?.get()?.let {
(it as FeedFragment).activityResumed()
}
}
fun tabSelected(tab: Tab) {
if (this.weakReferences.size >= 5) {
this.weakReferences.get(tab.identifier).get()?.selectedTab()
}
}
// /**
// * Return the fragment at the position key
// */
// fun getFragment(key: Int): BaseFragment? {
// if (this.weakReferences.get(key) != null) {
// return this.weakReferences.get(key).get()
// }
// return null
// }
}

@ -8,7 +8,6 @@ import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.viewmodel.ReportHolder
/**
@ -20,8 +19,8 @@ class ReportPagerAdapter(private val context: Context,
override fun getItem(position: Int): BaseFragment {
return when (position) {
0 -> GraphFragment.newInstance(Graph.Style.BAR)
1 -> GraphFragment.newInstance(Graph.Style.MULTILINE)
0 -> GraphFragment.newInstance(style = GraphFragment.Style.BAR)
1 -> GraphFragment.newInstance(style = GraphFragment.Style.MULTILINE)
2 -> {
val report = this.reportHolder.report
ComposableTableReportFragment.newInstance(report)
@ -34,7 +33,7 @@ class ReportPagerAdapter(private val context: Context,
return 3
}
override fun getPageTitle(position: Int): CharSequence {
override fun getPageTitle(position: Int): CharSequence? {
return when(position) {
0 -> context.getString(R.string.bars)
1 -> context.getString(R.string.lines)

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

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

@ -2,8 +2,8 @@ package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.*
import kotlinx.android.synthetic.main.fragment_comparison_chart.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentComparisonChartBinding
import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity
import net.pokeranalytics.android.ui.activity.SettingsActivity
import net.pokeranalytics.android.ui.adapter.ComparisonChartPagerAdapter
@ -12,7 +12,7 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.MoreTabRow
import net.pokeranalytics.android.ui.view.rowrepresentable.MoreTabRow
class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
@ -39,19 +39,11 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource
private lateinit var viewPagerAdapter: ComparisonChartPagerAdapter
private var comparisonChartMenu: Menu? = null
private var _binding: FragmentComparisonChartBinding? = null
private val binding get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentComparisonChartBinding.inflate(inflater, container, false)
return binding.root
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_comparison_chart, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -75,7 +67,7 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource
}
// Rows
override fun adapterRows(): List<RowRepresentable> {
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
@ -105,9 +97,9 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource
parentActivity?.let {
viewPagerAdapter = ComparisonChartPagerAdapter(requireContext(), it.supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.viewPager.offscreenPageLimit = 2
binding.tabs.setupWithViewPager(binding.viewPager)
viewPager.adapter = viewPagerAdapter
viewPager.offscreenPageLimit = 2
tabs.setupWithViewPager(viewPager)
}
}

@ -8,15 +8,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_data_list.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentCurrenciesBinding
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
@ -39,71 +39,42 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
private val mostUsedCurrencies = this.mostUsedCurrencyCodes.map { code ->
CurrencyRow(
this.systemCurrencies.first {
it.currencyCode == code
}
this.systemCurrencies.filter {
it.currencyCode == code
}.first()
)
}
private val availableCurrencies =
Locale.getAvailableLocales()
.mapNotNull {
try {
Currency.getInstance(it)
} catch (e: Exception) {
null
}
}.toSet()
.filter { !mostUsedCurrencyCodes.contains(it.currencyCode) }
.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 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 {
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 currencySymbol: String = this.currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})"
var currencyCode: String = currency.currencyCode
var currencySymbole: String = currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}
private var _binding: FragmentCurrenciesBinding? = null
private val binding get() = _binding!!
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCurrenciesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_currencies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -114,8 +85,8 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
// StaticRowRepresentableDataSource
override fun adapterRows(): List<RowRepresentable> {
return rowRepresentation
override fun adapterRows(): List<RowRepresentable>? {
return CurrenciesFragment.rowRepresentation
}
@ -149,7 +120,7 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
val viewManager = LinearLayoutManager(requireContext())
val dataListAdapter = RowRepresentableAdapter(this, this)
binding.recyclerView.apply {
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = dataListAdapter

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

Loading…
Cancel
Save