Compare commits

..

94 Commits

Author SHA1 Message Date
Laurent c751098e2f Bumps to 180 / 6.0.38 2 months ago
Laurent 3a8bf09500 fix issue with toolbar being hidden for some users 2 months ago
Laurent 439bbdacdd cleanup 2 months ago
Laurent d697ec13cc remove ffmpeg and use MediaMuxer instead 2 months ago
Laurent af7b5af456 upgrade realm version for 16kb pages 2 months ago
Laurent c0958694f2 bumps to 179 / 6.0.37 2 months ago
Laurent 99b90bb039 Fix toolbar not being visible for some user 2 months ago
Laurent 5e4a35dc81 attempt to fix issue with bottom buttons being hidden 2 months ago
Laurent e9d6cbe048 bumps to 6.0.35 2 months ago
Laurent 958de5c94f adds button to force Reports computation 2 months ago
Laurent e0153bdbd5 improve stakes formatting 2 months ago
Laurent 323022cc96 bumps to 6.0.34 2 months ago
Laurent 3f3982cc66 Fix issue where $0 was positive for tournaments 2 months ago
Laurent 1f61a7c99f Bumps to 6.0.33 2 months ago
Laurent bb189583d8 adds claude.md 2 months ago
Laurent fdcb2efe30 fixes bad padding 2 months ago
Laurent 1989e03072 fix issue with android 15 4 months ago
Laurent 63c90a5a8c upgrade to sdk 35 4 months ago
Laurent 79ffa5b6dc Bumps version to 6.0.30 / 172 8 months ago
Laurent 11fb5e595f Fix bad accounting 8 months ago
Laurent 113e2db78c Fix build issue + version bump 8 months ago
Laurent 9885a1e096 Fix crash 8 months ago
Laurent 75d9bb90c1 Bumps to 6.0.28 / 170 1 year ago
Laurent e713465aaf Fix crash 1 year ago
Laurent 07158f3e13 Fix library issue when submitting 1 year ago
Laurent 4135c1bd11 Bumps to 6.0.27 - 169 1 year ago
Laurent 3f883642b1 Fix test compilation 1 year ago
Laurent 5f131c6a65 Upgrade sdk from 33 to 34 and billing to 7.0 1 year ago
Laurent 2e4db055f4 Bumps to 6.0.26 / 168 1 year ago
Laurent 8a29187553 Logs exceptions coming from sending an email 1 year ago
Laurent 23872f625a Bumps to 167 / 6.0.25 1 year ago
Laurent 85f8ecbe21 Backup system now retries if it failed 1 year ago
Laurent d178540ceb gradle upgrade 1 year ago
Laurent 071b8c3aa1 Bumps to 166 / 6.0.23 1 year ago
Laurent a83ee58248 Fix pot label issue 1 year ago
Laurent f611e88ae5 Bumps to 6.0.22 / 165 2 years ago
Laurent d07793c409 Only add contested pots when considering winning pots 2 years ago
Laurent 829e64448f Fixes crash when opening the settings after having an opened keyboard in the players section 2 years ago
Laurent 4bab5687bc Bumps to 6.0.21 / 162 2 years ago
Laurent 37687db64a Fixes an issue where a player cannot go allin 2 years ago
Laurent 8bb25c506e Bumps to 6.0.20 / 163 2 years ago
Laurent 1d171f213c Fixes hand history stuff 2 years ago
Laurent 33c3479a02 Bumps to 6.0.19 / 162 2 years ago
Laurent 4583b5e12a Change the frequency of backups to 10 hours after the last change 2 years ago
Laurent b5769173b7 Bumps to 6.0.18 / 161 2 years ago
Laurent ab9b7724c1 Fix crash 2 years ago
Laurent 548d8b6acc Bumps 6.0.17 2 years ago
Laurent a1113903ea Fixes an issue where a lost pot was considered a winner for hero if he had chips returned 2 years ago
Laurent b0d0a00f15 Make database copy activity, only for debug purposes at the moment 2 years ago
Laurent e4aa792542 Fixes crash with backups 2 years ago
Laurent 98c2434365 Bumps to 158 / 6.0.15 2 years ago
Laurent 1560521e30 Fixes missing net result in BB graph 2 years ago
Laurent 77c23504f7 Fix keyboard not dismissing 2 years ago
Laurent 063d66fbca Bumps to 157 / 6.0.14 2 years ago
Laurent 6a300f1e10 Fixes icon 2 years ago
Laurent 6441c04bae Adds red dot when no email is set 2 years ago
Laurent e89b597de6 Bumps to 156 / 6.0.13 2 years ago
Laurent f0e711f8d1 Adds language edition 2 years ago
Laurent bd29ccfc11 Fix issue with hand editor 2 years ago
Laurent 4da6b4bcdc Attempt to fix multiple notification + version bump 2 years ago
Laurent 42b51c961c Bumps to 6.0.11 / 154 3 years ago
Laurent 703de02f26 Adds email backup feature 3 years ago
Laurent d4078032a4 Bumps to 6.0.10 / 153 3 years ago
Laurent 22bebf7a20 Leave screen when saving 3 years ago
Laurent 51bb26f331 Fix and improve currency related stuff 3 years ago
Laurent b316d32850 Fix look and feel 3 years ago
Laurent 363600fa9f Fixes crash 3 years ago
Laurent 0bf983c207 Bumps to 6.0.9 / 152 3 years ago
Laurent 2def8c8aeb Manage new and older way of displaying pictures 3 years ago
Laurent dd71fd9db9 Removes white test background 3 years ago
Laurent 1c456ab796 Adds image picking from library and fixes stuff 3 years ago
Laurent b0e88d08b1 Android 33 + refactoring of picture taking 3 years ago
Laurent 94305b9125 Bumps to 6.0.8 3 years ago
Laurent 6933b3ef06 Shows hands of player 3 years ago
Laurent 9bf524b1e3 Create PlayerDataViewModel to handle the Player representation 3 years ago
Laurent 7cb63575ac Adds hands count when importing from ios 3 years ago
Laurent 5ce359a41b Bumps to 6.0.7 3 years ago
Laurent 285bd334c7 Fixes 3 years ago
Laurent d695c3fc1a Bumps version to 6.0.6 3 years ago
Laurent 3ca625f898 avoid crash 3 years ago
Laurent 004bf5031a Fix lifecycle crashes 3 years ago
Laurent 7d7ae3bf82 bump version to 6.0.5 3 years ago
Laurent d404dd519e Removes useless suspend keyword 3 years ago
Laurent 102353db1e cleanup 3 years ago
Laurent d8d70e26ed cleanup and fixes attempt 3 years ago
Laurent b504f6741c Bumps to 6.0.4 3 years ago
Laurent 95a926aed5 minor code improvements 3 years ago
Laurent 0cb11ff0e4 Adds flag to avoid crash 3 years ago
Laurent d46d9597d2 hopefully fixes an unreproducible serialization crash 3 years ago
Laurent 602cd11849 comment poker base for now 3 years ago
Laurent 8ac5119019 Additional crash fixes 3 years ago
Laurent 1375bdef74 Fixes crashes 3 years ago
Laurent c0b022a553 Fix merge 3 years ago
Laurent d122f3fd53 Adds poker base import 3 years ago
  1. 139
      CLAUDE.md
  2. 67
      app/build.gradle
  3. 24
      app/proguard-rules.pro
  4. 3
      app/src/androidTest/java/net/pokeranalytics/android/components/RealmInstrumentedUnitTest.kt
  5. 6
      app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt
  6. 3
      app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt
  7. 5
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt
  8. 20
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  9. 3
      app/src/debug/AndroidManifest.xml
  10. 113
      app/src/main/AndroidManifest.xml
  11. BIN
      app/src/main/ic_launcher-playstore.png
  12. 83
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  13. 56
      app/src/main/java/net/pokeranalytics/android/RealmWriteService.kt
  14. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  15. 11
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  16. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  17. 1239
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  18. 7
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  19. 94
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  20. 16
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  21. 10
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  22. 21
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReportManager.kt
  23. 2
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  24. 1
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  25. 22
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  26. 27
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  27. 5
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  28. 7
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  29. 10
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  30. 2
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  31. 5
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  32. 59
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  33. 22
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  34. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  35. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  36. 26
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  37. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  38. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  39. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  40. 61
      app/src/main/java/net/pokeranalytics/android/model/realm/FlatTimeInterval.kt
  41. 9
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  42. 131
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  43. 91
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  44. 201
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  45. 27
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  46. 288
      app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt
  47. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  48. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt
  49. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  50. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  51. 15
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  52. 63
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  53. 14
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  54. 20
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  55. 209
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  56. 534
      app/src/main/java/net/pokeranalytics/android/model/utils/TimeManager.kt
  57. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  58. 31
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  59. 13
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  60. 51
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  61. 187
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt
  62. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt
  63. 13
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  64. 1
      app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt
  65. 16
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  66. 50
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  67. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  68. 42
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  69. 5
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt
  70. 85
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  71. 93
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  72. 51
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  73. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  74. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/Top10Fragment.kt
  75. 25
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  76. 24
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt
  77. 12
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt
  78. 79
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  79. 57
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  80. 12
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListFragment.kt
  81. 9
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt
  82. 21
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetMultiSelectionFragment.kt
  83. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStakesFragment.kt
  84. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStaticListFragment.kt
  85. 37
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt
  86. 54
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  87. 40
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt
  88. 2
      app/src/main/java/net/pokeranalytics/android/ui/helpers/AppReviewManager.kt
  89. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollFragment.kt
  90. 3
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt
  91. 185
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt
  92. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt
  93. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/CustomFieldDataFragment.kt
  94. 55
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/DataManagerFragment.kt
  95. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt
  96. 21
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataFragment.kt
  97. 80
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt
  98. 153
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataViewModel.kt
  99. 24
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt
  100. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/datalist/DataListFragment.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,139 @@
# 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,6 +1,6 @@
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
@ -12,13 +12,13 @@ apply plugin: "kotlinx-serialization"
repositories {
maven { url 'https://jitpack.io' } // required for MPAndroidChart
mavenCentral() // for kotlin serialization
jcenter() // for kotlin serialization
}
android {
compileSdkVersion 32
buildToolsVersion "30.0.2"
compileSdkVersion 35
buildToolsVersion "30.0.3"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -29,16 +29,13 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
lintOptions {
disable 'MissingTranslation'
}
defaultConfig {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 32
versionCode 147
versionName "6.0.4"
targetSdkVersion 35
versionCode 180
versionName "6.0.38"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -91,6 +88,10 @@ android {
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
@ -98,8 +99,8 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0-native-mt"
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-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"
@ -111,13 +112,16 @@ dependencies {
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:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places
implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions
implementation 'com.android.billingclient:billing:5.0.0'
implementation 'com.android.billingclient:billing:7.0.0'
// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -139,21 +143,38 @@ dependencies {
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'
// 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'
// https://mvnrepository.com/artifact/com.android.volley/volley
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
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'
// Test
testImplementation 'junit:junit:4.12'
testImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation 'androidx.test:rules:1.5.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2'
// gross, somehow needed to make the stop notif work
implementation 'com.google.guava:guava:27.0.1-android'

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

@ -20,9 +20,8 @@ open class RealmInstrumentedUnitTest {
fun newSessionInstance(realm: Realm, isCashGame: Boolean = true) : Session {
val session = realm.createObject(Session::class.java, UUID.randomUUID().toString())
session.startDate = Date()
session.endDate = Date()
session.type = if (isCashGame) Session.Type.CASH_GAME.ordinal else Session.Type.TOURNAMENT.ordinal
session.result = realm.createObject(Result::class.java, UUID.randomUUID().toString())
session.result = realm.createObject(Result::class.java)
return session
}

@ -34,7 +34,7 @@ class CriteriaTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val yearQueries = Criteria.Years.queryConditions(realm)
val yearQueries = Criteria.Years.queryConditions
assertEquals(16, yearQueries.size)
@ -57,7 +57,7 @@ class CriteriaTest : BaseFilterInstrumentedUnitTest() {
fun combined() {
val criterias = listOf(Criteria.MonthsOfYear, Criteria.DaysOfWeek)
val combined = criterias.combined(this.mockRealm)
val combined = criterias.combined()
val context = InstrumentationRegistry.getInstrumentation().context
combined.forEach {
@ -92,7 +92,7 @@ class CriteriaTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val context = InstrumentationRegistry.getInstrumentation().context
val allMonths = Criteria.AllMonthsUpToNow.queries(realm)
val allMonths = Criteria.AllMonthsUpToNow.queries
allMonths.forEach {
it.conditions.forEach { qc->
println("<<<<< ${qc.getDisplayName(context)}")

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

@ -16,7 +16,7 @@ import java.util.*
class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
private fun createDefaultTransactionTypes(realm: Realm) {
TransactionType.Value.values().forEachIndexed { _, value ->
TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType()
type.additive = value.additive
type.kind = value.uniqueIdentifier
@ -64,12 +64,10 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
val s1 = newSessionInstance(realm)
s1.bankroll = br1
s1.result?.cashout = 200.0
s1.preCompute()
val s2 = newSessionInstance(realm)
s2.bankroll = br2
s2.result?.cashout = 500.0
s2.preCompute()
}
@ -115,7 +113,6 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
val s1 = newSessionInstance(realm)
s1.bankroll = br1
s1.result?.cashout = 200.0
s1.endDate = Date()
}

@ -65,9 +65,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s1.location = l1
s2.location = l1
s1.preCompute()
s2.preCompute()
realm.commitTransaction()
assertEquals(2, computableResults.size)
@ -252,9 +249,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
// netDuration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
// netDuration = 4h, hourly = 100, bb100 = 150 / 75 * 100 = +200
s1.preCompute()
s2.preCompute()
}
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
@ -322,10 +316,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
s1.preCompute()
s2.preCompute()
s3.preCompute()
realm.commitTransaction()
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
@ -464,9 +454,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s1.endDate = ed1
realm.copyToRealmOrUpdate(s1)
s1.preCompute()
realm.commitTransaction()
val sets = realm.where(SessionSet::class.java).findAll()
@ -511,9 +498,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
s1.preCompute()
s2.preCompute()
realm.commitTransaction()
val sets = realm.where(SessionSet::class.java).findAll()
@ -747,10 +731,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s2.startDate = sd2
s2.endDate = ed2
s1.preCompute()
s2.preCompute()
}
val group = ComputableGroup(Query(), listOf())

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

@ -1,15 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.pokeranalytics.android">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<application
android:name=".PokerAnalyticsApplication"
@ -59,136 +62,186 @@
</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:screenOrientation="portrait"
android:exported="true" />
<!-- 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:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"/>
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<!-- 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:launchMode="singleTop"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<service android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService" android:exported="false"/>
<service android:name="net.pokeranalytics.android.RealmWriteService" 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.

After

Width:  |  Height:  |  Size: 161 KiB

@ -1,12 +1,8 @@
package net.pokeranalytics.android
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.IBinder
import com.google.firebase.FirebaseApp
import io.realm.Realm
import io.realm.RealmConfiguration
@ -15,30 +11,20 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.FlatTimeInterval
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.TimeManager
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber
import java.util.*
object AppState {
var isImporting = false
}
class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
private var realmWriteService: RealmWriteService? = null
var backupOperator: BackupOperator? = null
companion object {
@ -65,14 +51,13 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(15)
.schemaVersion(14)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build()
Realm.setDefaultConfiguration(realmConfiguration)
initRealmWriteService()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val locales = resources.configuration.locales
CrashLogging.log("App onCreate. Locales = $locales")
@ -94,65 +79,21 @@ class PokerAnalyticsApplication : Application() {
// Patch
Patcher.patchAll(this)
// Reports
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
TimeManager.configure()
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// Infos
val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Debugging
val realm = Realm.getDefaultInstance()
val emptyFTI = realm.where(FlatTimeInterval::class.java).isEmpty("sessions").findAll()
if (emptyFTI.isNotEmpty()) {
Timber.w(">>> WARNING: There are ${emptyFTI.size} FTIs without sessions")
// Timber.w(">>> DELETING THE EMPTY FTIs")
// realm.executeTransactionAsync {
// it.where(FlatTimeInterval::class.java).isEmpty("sessions").findAll().deleteAllFromRealm()
// }
}
val ftis = realm.where(FlatTimeInterval::class.java).sort("startDate").findAll()
Timber.d(">>> Total FTIs count = ${ftis.size}")
ftis.forEach {
Timber.d("fti > ${it.startDate} / ${it.endDate}")
}
Timber.d("================")
val sessions = realm.where<Session>().findAll()
sessions.forEach {
Timber.d("Session FTI count = ${it.flatTimeIntervals.size}")
}
}
/** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
realmWriteService = null
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as RealmWriteService.LocalBinder
realmWriteService = binder.getService()
}
}
private fun initRealmWriteService() {
val intent = Intent(this, RealmWriteService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onTerminate() {
super.onTerminate()
unbindService(connection)
}
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
fun executeRealmAsyncTransaction(handler: (Realm) -> (Unit)) {
this.realmWriteService?.executeRealmAsyncTransaction(handler) ?: throw PAIllegalStateException("no realmWriteService")
}
/**
@ -165,7 +106,7 @@ class PokerAnalyticsApplication : Application() {
realm.close()
if (sessionsCount < 10) {
CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(context = Dispatchers.IO).launch {
FakeDataManager.createFakeSessions(500)
}
}

@ -1,56 +0,0 @@
package net.pokeranalytics.android
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import io.realm.Realm
import timber.log.Timber
class RealmWriteService : Service() {
private lateinit var realm: Realm
private val binder = LocalBinder()
override fun onBind(intent: Intent?): IBinder {
return binder
}
inner class LocalBinder : Binder() {
fun getService(): RealmWriteService = this@RealmWriteService
}
override fun onCreate() {
super.onCreate()
this.realm = Realm.getDefaultInstance()
}
override fun onDestroy() {
super.onDestroy()
Timber.d(">>>> Service destroyed : realm close")
this.realm.close()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
fun executeRealmAsyncTransaction(handler: (Realm) -> (Unit)) {
Timber.d(">>>> Launch async transaction...")
this.realm.executeTransactionAsync({ asyncRealm ->
handler(asyncRealm)
Timber.d(">> transaction handler done")
}, {
Timber.d(">> onSuccess, refreshing...")
this.realm.refresh()
}, {
Timber.d(">> transaction failed: $it")
})
}
}

@ -0,0 +1,75 @@
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,6 +1,7 @@
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
@ -9,8 +10,11 @@ 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)
@ -18,18 +22,13 @@ class CurrencyConverterApi {
companion object {
private val json = Json { ignoreUnknownKeys = true }
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://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=9b56e742a75392c8aeb7"
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 }
Timber.d("Api call = $url")
val stringRequest = object : StringRequest(

@ -0,0 +1,242 @@
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
}
}
}

@ -53,8 +53,6 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
}
}
// Timber.d("QUERY = ${query.defaultName}")
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
@ -64,11 +62,6 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
return computables
}
fun timeIntervals(realm: Realm): RealmResults<FlatTimeInterval> {
return Filter.queryOn(realm, this.query)
}
/**
* The list of sets to compute
*/

@ -1,26 +1,22 @@
package net.pokeranalytics.android.calculus
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.CountDownTimer
import androidx.localbroadcastmanager.content.LocalBroadcastManager
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.AppState
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.ImportBroadcast
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()
@ -30,7 +26,6 @@ class ReportWhistleBlower(var context: Context) {
private var sessions: RealmResults<Session>? = null
private var results: RealmResults<Result>? = null
private var sessionSets: RealmResults<SessionSet>? = null
private var currentTask: ReportTask? = null
@ -38,42 +33,25 @@ class ReportWhistleBlower(var context: Context) {
private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
private var timer: CountDownTimer? = null
var paused: Boolean = false
private val startImportReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
pause()
}
}
private val endImportReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
resume()
}
}
private var timer: CountDownTimer? = null
init {
val realm = Realm.getDefaultInstance()
sessions = realm.where(Session::class.java).findAll()
sessions?.addChangeListener { _ ->
requestReportLaunch()
}
results = realm.where(Result::class.java).findAll()
results?.addChangeListener { _ ->
this.sessions = realm.where(Session::class.java).findAll()
this.sessions?.addChangeListener { _ ->
requestReportLaunch()
}
sessionSets = realm.where(SessionSet::class.java).findAll()
sessionSets?.addChangeListener { _ ->
this.results = realm.where(Result::class.java).findAll()
this.results?.addChangeListener { _ ->
requestReportLaunch()
}
realm.close()
LocalBroadcastManager.getInstance(context).registerReceiver(startImportReceiver, IntentFilter(ImportBroadcast.START.identifier))
LocalBroadcastManager.getInstance(context).registerReceiver(endImportReceiver, IntentFilter(ImportBroadcast.END.identifier))
}
fun addListener(newPerformanceListener: NewPerformanceListener) {
@ -85,8 +63,10 @@ class ReportWhistleBlower(var context: Context) {
}
fun requestReportLaunch() {
// Timber.d(">>> Launch report")
if (AppState.isImporting) {
if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return
}
@ -120,12 +100,14 @@ class ReportWhistleBlower(var context: Context) {
/**
* Pauses the whistleblower, for example when importing data
*/
private fun pause() {
fun pause() {
this.paused = true
this.currentTask?.cancel()
this.currentTask = null
}
private fun resume() {
fun resume() {
this.paused = false
this.requestReportLaunch()
}
@ -151,20 +133,24 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private var cancelled = false
var handler: (() -> Unit)? = null
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() {
messages.add("Starting task...")
launchReports()
}
fun cancel() {
// Timber.d("Reportwhistleblower task CANCEL")
this.cancelled = true
}
private fun launchReports() {
// Timber.d("====== Report whistleblower launch batch...")
var messages: MutableList<String> = mutableListOf()
CoroutineScope(Dispatchers.Default).launch {
private fun launchReports() {
CoroutineScope(coroutineContext).launch {
val realm = Realm.getDefaultInstance()
@ -178,8 +164,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
// CustomField
val customFields = realm.where(CustomField::class.java)
.equalTo("type", CustomField.Type.LIST.uniqueIdentifier)
.findAll()
.equalTo("type", CustomField.Type.LIST.uniqueIdentifier).findAll()
for (customField in customFields) {
if (cancelled) {
break
@ -212,17 +197,21 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.values().forEach { key ->
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 staticReport.performanceStats) {
for (stat in result.options.stats) {
// Timber.d("analyse stat: $stat for report: $staticReport")
@ -239,15 +228,17 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
// 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)
val count = computedResults.computedStat(Stat.NUMBER_OF_GAMES)?.value?.toInt() ?: throw PAIllegalStateException("Number of games not found")
val notify = !(stat.hasEarlyVariance && count < 10)
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
@ -265,13 +256,11 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id
}
if (notify) {
this.whistleBlower.notify(currentPerf)
}
this.whistleBlower.notify(currentPerf)
}
}
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) {
val performance = Performance(
@ -283,12 +272,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
null
)
realm.executeTransaction { it.copyToRealm(performance) }
if (notify) {
this.whistleBlower.notify(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 {

@ -55,8 +55,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31),
FTI_COUNT(32)
TOTAL_TIPS(31)
;
companion object : IntSearchable<Stat> {
@ -137,7 +136,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
TOTAL_BUYIN -> R.string.total_buyin
TOURNAMENT_ITM_RATIO -> R.string.itm_ratio
TOTAL_TIPS -> R.string.total_tips
FTI_COUNT -> R.string.players_count
else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined")
}
}
@ -164,7 +162,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return TextFormat(value.formatted, color)
}
// white integers
NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED, FTI_COUNT -> {
NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> {
return TextFormat("${value.toInt()}")
} // white durations
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
@ -210,7 +208,7 @@ 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 -> R.string.total
NET_RESULT, BB_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
@ -321,14 +319,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
}
}
val hasEarlyVariance: Boolean
get() {
return when (this) {
HOURLY_RATE, AVERAGE, AVERAGE_NET_BB, AVERAGE_BUYIN, AVERAGE_HOURLY_DURATION -> true
else -> false
}
}
private val hasProgressValues: Boolean
get() {
return when (this) {

@ -31,7 +31,7 @@ class BankrollCalculator {
var initialValue = 0.0
var transactionNet = 0.0
for (bankroll in bankrolls) {
bankrolls.forEach { bankroll ->
val rate = if (setup.virtualBankroll) bankroll.rate else 1.0
@ -70,8 +70,8 @@ class BankrollCalculator {
val transactions = Filter.queryOn<Transaction>(realm, baseQuery)
report.addDatedItems(transactions)
for (transaction in transactions) {
report.addTransaction(transaction)
transactions.forEach {
report.addTransaction(it)
}
val sessionQuery = Query(QueryCondition.DateNotNull).merge(baseQuery)
@ -83,8 +83,8 @@ class BankrollCalculator {
val options = Calculator.Options(stats = listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY))
val group = ComputableGroup(baseQuery)
val result = Calculator.compute(realm, group, options)
result.computedStat(Stat.NET_RESULT)?.let { computedStat ->
report.netResult = computedStat.value
result.computedStat(Stat.NET_RESULT)?.let {
report.netResult = it.value
}
this.computeRiskOfRuin(report, result)

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

@ -64,7 +64,7 @@ class CashGameOptimalDurationCalculator {
var end: Double? = null
var validBuckets = 0
// val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
// Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0

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

@ -25,12 +25,11 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
fun List<Criteria>.combined(realm: Realm): List<Query> {
fun List<Criteria>.combined(): List<Query> {
val comparatorList = ArrayList<List<Query>>()
this.forEach { criteria ->
comparatorList.add(criteria.queries(realm))
comparatorList.add(criteria.queries)
}
return getCombinations(comparatorList)
}
@ -65,10 +64,11 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
inline fun <reified T : NameManageable> comparison(): List<Query> {
if (this is ListCustomFields) {
val objects = mutableListOf<QueryCondition.CustomFieldListQuery>()
val realm = Realm.getDefaultInstance()
val realm = Realm.getDefaultInstance()
realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
objects.add(QueryCondition.CustomFieldListQuery(it))
}
objects.sort()
realm.close()
return objects.map { Query(it) }
}
@ -166,11 +166,14 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
fun queries(realm: Realm): List<Query> {
val queries: List<Query>
get() {
return when (this) {
is AllMonthsUpToNow -> {
val realm = Realm.getDefaultInstance()
val firstSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.ASCENDING).findFirst()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst()
realm.close()
val years: ArrayList<Query> = arrayListOf()
@ -198,12 +201,13 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
years
}
else -> {
return this.queryConditions(realm)
return this.queryConditions
}
}
}
fun queryConditions(realm: Realm): List<Query> {
val queryConditions: List<Query>
get() {
return when (this) {
is Bankrolls -> comparison<Bankroll>()
is Games -> comparison<Game>()
@ -218,6 +222,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is TournamentFees -> comparison<QueryCondition.TournamentFee, Double>()
is Years -> {
val years = arrayListOf<Query>()
val realm = Realm.getDefaultInstance()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst()
val yearNow = lastSession?.year ?: return years
@ -229,16 +234,19 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
years.add(Query(yearCondition))
}
}
realm.close()
years
}
is Stakes -> comparison<QueryCondition.AnyStake, 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)
}
realm.close()
queries
}
is Duration -> {

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

@ -2,6 +2,7 @@ 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
@ -119,7 +120,7 @@ fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
.addTag(this.id)
.build()
WorkManager.getInstance(context).enqueue(work)
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
}
@ -133,7 +134,7 @@ val AbstractList<Session>.hourlyDuration: Double
return intervals.sumOf { it.hourlyDuration }
}
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long = 0L) {
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {
val hourlyDuration: Double
get() {

@ -32,9 +32,9 @@ import net.pokeranalytics.android.util.CrashLogging
*
*/
//class UnmanagedFilterField(message: String) : Exception(message) {
//
//}
class UnmanagedFilterField(message: String) : Exception(message) {
}
/**
* Companion-level Interface to indicate an RealmObject class can be filtered and to provide all the fieldNames (eg: parameter's path) needed to be query on.
@ -64,7 +64,6 @@ class FilterHelper {
SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition)
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition)
FlatTimeInterval::class.java -> FlatTimeInterval.fieldNameForQueryType(queryCondition)
else -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null

@ -73,16 +73,16 @@ class Query {
it is QueryCondition.EndedToTime
}
for (condition in this.conditions) {
realmQuery = when (condition) {
this.conditions.forEach {
realmQuery = when (it) {
is QueryCondition.StartedFromTime -> {
condition.queryWith(realmQuery, queryToTime)
it.queryWith(realmQuery, queryToTime)
}
is QueryCondition.EndedToTime -> {
condition.queryWith(realmQuery, queryFromTime)
it.queryWith(realmQuery, queryFromTime)
}
else -> {
condition.queryWith(realmQuery)
it.queryWith(realmQuery)
}
}
}

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

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

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

@ -3,7 +3,6 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.FlatTimeInterval
import timber.log.Timber
import java.util.*
@ -336,27 +335,6 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++
}
// Migrate to version 15
if (currentVersion == 14) {
schema.get("Result")?.let { crs ->
crs.addField("id", String::class.java).setRequired("id", true)
crs.addPrimaryKey("id")
}
schema.create("FlatTimeInterval")?.let { fs ->
fs.addField("id", String::class.java).setRequired("id", true)
fs.addPrimaryKey("id")
fs.addField("startDate", Date::class.java).setRequired("startDate", true)
fs.addField("endDate", Date::class.java).setRequired("endDate", true)
fs.addField("duration", Long::class.java)
schema.get("Session")?.let { ss ->
ss.addRealmSetField("flatTimeIntervals", fs)
}
}
currentVersion++
}
}
override fun equals(other: Any?): Boolean {

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

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

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

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

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

@ -63,6 +63,16 @@ 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 {
return when (T::class) {
Int::class -> intValue ?: 0

@ -1,61 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import java.util.*
@RealmClass
open class FlatTimeInterval : RealmObject(), Filterable {
@PrimaryKey
var id = UUID.randomUUID().toString()
/**
* The start date of the session
*/
var startDate: Date = Date()
set(value) {
field = value
this.computeDuration()
}
/**
* The start date of the session
*/
var endDate: Date = Date()
set(value) {
field = value
this.computeDuration()
}
/**
* the net duration of the session, automatically calculated
*/
var duration: Long = 0L
@LinkingObjects("flatTimeIntervals")
val sessions: RealmResults<Session>? = null
private fun computeDuration() {
duration = endDate.time - startDate.time
}
companion object {
fun fieldNameForQueryType(queryCondition: Class <out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
}

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

@ -4,27 +4,20 @@ 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.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
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.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.RANDOM_PLAYER
import net.pokeranalytics.android.util.extensions.isSameDay
import net.pokeranalytics.android.util.extensions.mediumDate
import java.util.*
import kotlin.collections.ArrayList
open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable {
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -44,13 +37,6 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
@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
@ -69,70 +55,16 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
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) {
PlayerPropertiesRow.NAME -> this.name.ifEmpty { 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? ?: ""
}
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(PlayerPropertiesRow.IMAGE)
rows.add(PlayerPropertiesRow.NAME)
rows.add(PlayerPropertiesRow.SUMMARY)
if (comments.size > 0) {
// Adds Comments section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, R.string.comments))
val currentCommentCalendar = Calendar.getInstance()
val currentDateCalendar = Calendar.getInstance()
val commentsToDisplay = ArrayList<Comment>()
commentsToDisplay.addAll(comments)
commentsToDisplay.sortByDescending { it.date }
commentsToDisplay.forEachIndexed { index, comment ->
currentCommentCalendar.time = comment.date
if (!currentCommentCalendar.isSameDay(currentDateCalendar) || index == 0) {
currentDateCalendar.time = currentCommentCalendar.time
// Adds day sub section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_SUBTITLE, title = currentDateCalendar.time.mediumDate()))
}
// Adds comment
rows.add(comment)
}
rows.add(SeparatorRow())
PlayerPropertiesRow.IMAGE -> this.picture = value as? String
}
return rows
}
/**
@ -142,57 +74,6 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
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.executeTransactionAsync {
this.commentsToDelete.forEach { // entries are out of realm
realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
}
}
realm.close()
this.commentsToDelete.clear()
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
when (row) {
PlayerPropertiesRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name))
PlayerPropertiesRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.summary))
}
return null
}
val initials: String
get() {
return if (this.name.isNotEmpty()) {
@ -213,4 +94,8 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
}
}
fun hands(realm: Realm): RealmResults<HandHistory> {
return realm.where(HandHistory::class.java).equalTo("playerSetups.player.id", this.id).findAll()
}
}

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

@ -27,13 +27,16 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.filter.QueryCondition.*
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.TimeManager
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.extensions.*
import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.extensions.shortDateTime
import net.pokeranalytics.android.util.extensions.toCurrency
import net.pokeranalytics.android.util.extensions.toMinutes
import java.text.DateFormat
import java.text.NumberFormat
import java.text.ParseException
@ -64,32 +67,24 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
companion object {
fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session {
fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null, managed: Boolean = true): Session {
val session = Session()
session.result = Result()
if (bankroll != null) {
session.bankroll = realm.copyFromRealm(bankroll)
session.bankroll = bankroll
} else {
realm.where<Bankroll>().findFirst()?.let { br ->
session.bankroll = realm.copyFromRealm(br)
}
session.bankroll = realm.where<Bankroll>().findFirst()
}
session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal
session.limit = Limit.NO.ordinal
session.game = realm.where(Game::class.java).equalTo("shortName", "HE").findFirst()
realm.where(Game::class.java)
.equalTo("shortName", "HE").findFirst()?.let {
session.game = realm.copyFromRealm(it)
return if (managed) {
realm.copyToRealm(session)
} else {
session
}
return session
// return if (managed) {
// realm.copyToRealm(session)
// } else {
// session
// }
}
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
@ -162,7 +157,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
@Ignore
val computableResult: ComputableResult? = this.computableResults?.firstOrNull()
// Timed interface
override var dayOfWeek: Int? = null
@ -199,7 +193,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
*/
var startDate: Date? = null
set(value) {
val previous = this.startDate
field = value
if (value == null) {
startDateHourMinuteComponent = null
@ -211,14 +204,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.updateTimeParameter(field)
this.computeNetDuration()
// nullifies the endDate when setting the start date after the end date
// nullifies enddate when setting the start date after the end date
if (value != null && this.endDate != null && value.after(this.endDate)) {
this.endDate = null
}
TimeManager.startChanged(this, min(previous, value))
// this.computeStats()
this.dateChanged()
this.computeStats()
}
/**
@ -227,7 +218,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
@Index
var endDate: Date? = null
set(value) {
val previous = this.endDate
field = value
if (value == null) {
endDateHourMinuteComponent = null
@ -238,9 +228,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
}
this.computeNetDuration()
TimeManager.endChanged(this, max(previous, value))
this.dateChanged()
this.defineDefaultTournamentBuyinIfNecessary()
// this.computeStats()
this.computeStats()
}
/**
@ -250,7 +240,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) {
field = value
this.computeNetDuration()
// this.computeStats()
this.computeStats()
}
/**
@ -262,6 +252,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* The start date of the break
*/
override var pauseDate: Date? = null
set(value) {
field = value
// this.updateRowRepresentation()
}
// The session set containing the sessions, which can contain multiple endedSessions
var sessionSet: SessionSet? = null
@ -274,7 +268,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) {
field = value
this.generateStakes()
// this.computeStats()
this.computeStats()
// this.updateRowRepresentation()
}
@ -302,7 +296,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
set(value) {
if (value > 0) {
field = value
// this.computeStats()
this.computeStats()
}
}
@ -320,13 +314,16 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The small blind value
var cgOldSmallBlind: Double? = null
set(value) {
field = value
}
// The big blind value
var cgOldBigBlind: Double? = null
set(value) {
field = value
this.computeStats()
// this.result?.computeNumberOfRebuy()
this.result?.computeNumberOfRebuy()
}
// var blinds: String? = null
@ -337,8 +334,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
field = value
this.generateStakes()
this.defineHighestBet()
// this.computeStats()
// this.result?.computeNumberOfRebuy()
this.computeStats()
this.result?.computeNumberOfRebuy()
}
var cgBlinds: String? = null
@ -346,8 +343,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
field = cleanupBlinds(value)
this.generateStakes()
this.defineHighestBet()
// this.computeStats()
// this.result?.computeNumberOfRebuy()
this.computeStats()
this.result?.computeNumberOfRebuy()
}
var cgBiggestBet: Double? = null
@ -358,6 +355,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The entry fee of the tournament
var tournamentEntryFee: Double? = null
set(value) {
field = value
this.result?.computeNumberOfRebuy()
}
// The total number of players who participated in the tournament
var tournamentNumberOfPlayers: Int? = null
@ -374,29 +375,29 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The custom fields values
var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()
// The list of opponents who participated to the session
var flatTimeIntervals: RealmList<FlatTimeInterval> = RealmList()
// The number of hands played during the sessions
var handsCount: Int? = null
set(value) {
field = value
this.computeStats()
}
fun bankrollHasBeenUpdated() {
this.generateStakes()
}
// /**
// * Manages impacts on SessionSets
// * Should be called when the start / end date are changed
// */
// private fun dateChanged() {
// SessionSetManager.updatedSession(this)
//// if (this.endDate != null) {
//// SessionSetManager.updateTimeline(this)
//// } else if (this.sessionSet != null) {
//// SessionSetManager.removeFromTimeline(this)
//// }
//// this.updateRowRepresentation()
// }
/**
* Manages impacts on SessionSets
* Should be called when the start / end date are changed
*/
private fun dateChanged() {
if (this.endDate != null) {
SessionSetManager.updateTimeline(this)
} else if (this.sessionSet != null) {
SessionSetManager.removeFromTimeline(this)
}
// this.updateRowRepresentation()
}
/**
* Returns a non-null date for the session
@ -435,7 +436,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
get() {
val bb = this.cgBiggestBet
val result = this.result
return if (bb != null && bb > 0.0 && result != null) {
return if (bb != null && result != null) {
result.net / bb
} else {
0.0
@ -470,11 +471,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
return this.result?.net ?: 0.0
}
fun preCompute() {
this.computeStats()
this.result?.computeNumberOfRebuy()
}
/**
* Pre-compute various statIds
*/
@ -497,21 +493,16 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
}
}
this.sessionSet?.computeStats()
}
/**
* Approximates the number of hands played per hour at the table
*/
private val numberOfHandsPerHour: Double
val numberOfHandsPerHour: Double
get() {
val tableSize = this.tableSize ?: 9 // 9 is the default table size if null
var playerHandsPerHour = 0
UserConfig.getConfiguration(this.realm) { config ->
playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour
}
val config = UserConfig.getConfiguration(this.realm)
val playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour
return this.numberOfTables * playerHandsPerHour / tableSize.toDouble()
}
@ -566,7 +557,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* Start or continue a session
*/
fun startOrContinue() {
// realm.executeTransaction {
realm.executeTransaction {
when (val state = getState()) {
SessionState.PENDING, SessionState.PLANNED -> {
this.startDate = Date()
@ -585,7 +576,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
throw PAIllegalStateException("unmanaged session state: $state")
}
}
// }
}
}
private fun defineDefaultTournamentBuyinIfNecessary() {
@ -598,28 +589,28 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* Pause a session
*/
fun pause() {
// realm.executeTransaction {
realm.executeTransaction {
when (val state = getState()) {
SessionState.STARTED -> {
this.pauseDate = Date()
}
else -> throw PAIllegalStateException("Pausing a session in an unmanaged state: $state")
}
// }
}
}
/**
* Stop a session
*/
fun stop(context: Context) {
// realm.executeTransaction {
realm.executeTransaction {
when (val state = getState()) {
SessionState.STARTED, SessionState.PAUSED -> {
this.end()
}
else -> throw Exception("Stopping session in unmanaged state: $state")
}
// }
}
cancelStopNotification(context)
}
@ -627,12 +618,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
* Restart a session
*/
fun restart() {
// realm.executeTransaction {
realm.executeTransaction {
this.pauseDate = null
this.startDate = Date()
this.endDate = null
this.breakDuration = 0L
// }
}
}
/**
@ -669,7 +660,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
/**
* Return the game titleResId
* Example: NL Hold'em
* Example: NL Holdem
*/
fun getFormattedGame(): String {
var gameTitle = ""
@ -681,7 +672,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
if (game != null) {
gameTitle += game?.name
}
return gameTitle.ifBlank { NULL_TEXT }
return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT
}
fun getFormattedStakes(): String {
@ -695,10 +686,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
*/
fun delete() {
CrashLogging.log("Deletes session. Id = ${this.id}")
if (isValid) {
cleanup()
deleteFromRealm()
// CrashLogging.log("Deletes session. Id = ${this.id}")
realm.executeTransaction {
cleanup()
deleteFromRealm()
}
} else {
CrashLogging.log("Attempt to delete an invalid session")
}
@ -711,15 +704,11 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// Updates the timeline
this.sessionSet?.let {
TimeManager.removeFromTimeline(this)
SessionSetManager.removeFromTimeline(this)
}
TimeManager.sessionDateChanged(this)
// cleanup unnecessary related objects
this.flatTimeIntervals.deleteAllFromRealm()
this.result?.deleteFromRealm()
this.computableResult?.deleteFromRealm()
this.computableResults?.deleteAllFromRealm()
}
@ -774,16 +763,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
val localResult = getOrCreateResult()
localResult.cashout = value as Double?
if (value != null) {
this.end()
}
}
SessionPropertiesRow.NET_RESULT -> {
val localResult = getOrCreateResult()
localResult.netResult = value as Double?
if (value != null) {
this.end()
}
}
SessionPropertiesRow.COMMENT -> comment = value as String? ?: ""
SessionPropertiesRow.END_DATE -> if (value is Date?) {
@ -839,23 +822,20 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName?
SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal
SessionPropertiesRow.TOURNAMENT_FEATURE -> {
this.tournamentFeatures.clear()
value?.let {
tournamentFeatures = RealmList()
tournamentFeatures.addAll((it as List<TournamentFeature>))
tournamentFeatures.addAll((it as ArrayList<TournamentFeature>))
} ?: run {
tournamentFeatures.removeAll(this.tournamentFeatures)
}
}
SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt()
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables = (value as Double?)?.toInt() ?: 1
is CustomField -> {
val entryIds = row.entries.map { it.id }
val entries = this.customFieldEntries.intersectBy(entryIds) { it.id }
this.customFieldEntries.removeAll(entries)
// customFieldEntries.filter { it.customField?.id == row.id }.let {
// customFieldEntries.removeAll(it.toSet())
// }
customFieldEntries.filter { it.customField?.id == row.id }.let {
customFieldEntries.removeAll(it)
}
when (row.type) {
CustomField.Type.AMOUNT.uniqueIdentifier,
CustomField.Type.NUMBER.uniqueIdentifier -> {
@ -880,11 +860,10 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
private fun getOrCreateResult(): Result {
return this.result
?: run {
val result = Result()
// result.inverseSession = WeakReference(this)
this.result = result
result
}
val result = realm.createObject(Result::class.java)
this.result = result
result
}
}
// Stat Entry
@ -918,7 +897,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.bbNet,
this.estimatedHands
)
Stat.AVERAGE_NET_BB -> this.bbNet
Stat.AVERAGE_NET_BB, Stat.BB_NET_RESULT -> this.bbNet
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
Stat.HANDS_PLAYED -> this.estimatedHands
@ -995,7 +974,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.COMMENT -> this.comment.ifEmpty { NULL_TEXT }
SessionPropertiesRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.GAME -> getFormattedGame()
SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
@ -1030,10 +1009,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context)
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString()
is CustomField -> {
val entryIds = this.customFieldEntries.map { it.id }
val entries = row.entries.intersectBy(entryIds) { it.id }
entries.firstOrNull()?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(row, currency)
customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(currency)
}
return NULL_TEXT
}

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

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

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

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

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

@ -31,7 +31,8 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
BONUS(2, true),
STACKING_INCOMING(3, true),
STACKING_OUTGOING(4, false),
TRANSFER(5, false);
TRANSFER(5, false),
EXPENSE(6, false); // not created by default, only used for poker base import atm
companion object : IntSearchable<Value> {
@ -49,6 +50,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
STACKING_INCOMING -> R.string.stacking_incoming
STACKING_OUTGOING -> R.string.stacking_outgoing
TRANSFER -> R.string.transfer
EXPENSE -> R.string.expense
}
}
@ -70,6 +72,10 @@ 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) {

@ -5,26 +5,13 @@ import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
import java.util.*
open class UserConfig : RealmObject() {
companion object {
fun getConfiguration(realm: Realm?, handler: (UserConfig) -> (Unit)) {
if (realm != null) {
handler(userConfiguration(realm))
} else {
val r = Realm.getDefaultInstance()
handler(userConfiguration(r))
r.close()
}
}
private fun userConfiguration(realm: Realm): UserConfig {
fun getConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config
}

@ -190,9 +190,11 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/***
* Configures a hand history with a [handSetup]
*/
fun configure(handSetup: HandSetup) {
fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
this.playerSetups.removeAll(this.playerSetups)
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it }
@ -313,7 +315,13 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
* Creates and affect a PlayerSetup at the given [positionIndex]
*/
fun createPlayerSetup(positionIndex: Int): PlayerSetup {
val playerSetup = PlayerSetup()
val playerSetup = if (this.realm != null) {
this.realm.createObject(PlayerSetup::class.java) }
else {
PlayerSetup()
}
playerSetup.position = positionIndex
this.playerSetups.add(playerSetup)
return playerSetup
@ -471,7 +479,12 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean?
get() {
return this.heroIndex?.let { heroIndex ->
this.winnerPots.any { it.position == heroIndex }
this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
} ?: run {
null
}
@ -556,7 +569,7 @@ 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 }} ")
Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map {it.position}} ")
}
/***
@ -633,19 +646,22 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pots.forEach { pot ->
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
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
}
}
}
@ -727,7 +743,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return boardHasWildCard || playerCardHasWildCard
}
val allFullCards: List<Card>
private val allFullCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
@ -747,4 +763,13 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
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
}
}
}

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

@ -17,15 +17,19 @@ class Seed(var context:Context) : Realm.Transaction {
fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) {
values.forEach { value ->
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)
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)
}
}
}
}
}

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

@ -1,534 +0,0 @@
package net.pokeranalytics.android.model.utils
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmQuery
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.realm.FlatTimeInterval
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.max
import net.pokeranalytics.android.util.extensions.min
import timber.log.Timber
import java.util.*
class CorruptSessionSetException(message: String) : Exception(message)
/**
* The TimeManager pre-computes time related data:
* - SessionSet: All overlapping sessions are grouped into a SessionSet,
* used to calculate the number of sessions and break durations
* - FlatTimeInterval: Sessions time intervals are breaked down into smaller intervals
* when overlapping occurs to get faster duration calculations
*/
object TimeManager {
var sessions: RealmResults<Session>? = null
private val sessionIdsToProcess = mutableSetOf<String>()
private var start: Date? = null
private var end: Date? = null
fun configure() {} // launch init
fun startChanged(session: Session, date: Date?) {
this.start = min(this.start, date)
this.end = max(this.end, session.endDate)
this.sessionIdsToProcess.add(session.id)
}
fun endChanged(session: Session, date: Date?) {
this.end = max(this.end, date)
this.start = min(this.start, session.startDate)
this.sessionIdsToProcess.add(session.id)
}
fun sessionDateChanged(session: Session) {
this.start = min(this.start, session.startDate)
this.end = max(this.end, session.endDate)
this.sessionIdsToProcess.add(session.id)
}
init {
val realm = Realm.getDefaultInstance()
sessions = realm.where(Session::class.java).findAllAsync()
sessions?.addChangeListener { _, _ ->
if (sessionIdsToProcess.isNotEmpty()) {
realm.executeTransactionAsync({ asyncRealm ->
val sessions = sessionIdsToProcess.mapNotNull { asyncRealm.findById<Session>(it) }
sessionIdsToProcess.clear()
for (session in sessions) {
Timber.d("Session id = ${session.id}")
Timber.d("Session time intervals count = ${session.flatTimeIntervals.size}")
session.flatTimeIntervals.deleteAllFromRealm()
val fti = FlatTimeInterval()
session.flatTimeIntervals.add(fti)
asyncRealm.insertOrUpdate(session)
}
}, {
Timber.d("executeTransactionAsync onSuccess listener...")
val timeIntervals = realm.where(FlatTimeInterval::class.java).findAll()
Timber.d("Total timeIntervals count = ${timeIntervals.size}")
timeIntervals.forEach {
Timber.d(">>> Time interval session count = ${it.sessions?.size}, session id = ${it.sessions?.firstOrNull()?.id}")
}
}, {})
}
}
// sessions?.addChangeListener { _, _ ->
//
// Timber.d("...sessions change at ${Date().time}")
//
// val start = this.start
// val end = this.end
// if (start != null && end != null) {
//
// Timber.d("...process date changes from $start to $end")
//
// this.start = null
// this.end = null
//
// realm.executeTransactionAsync ({ asyncRealm ->
// processSessions(asyncRealm, start, end)
// cleanUp()
// }, {
// Timber.d(">>>>> ON SUCCESS")
//
// realm.where(FlatTimeInterval::class.java).findAll().forEach {
// Timber.d("######## sessions count = ${it.sessions?.size}")
// }
//
// }, {
// Timber.d("Transaction failed : $it")
// })
// }
// }
realm.close()
}
private fun cleanUp() {
this.start = null
this.end = null
this.sessionIdsToProcess.clear()
}
private fun processSessions(realm: Realm, start: Date, end: Date) {
Timber.d("***** processSessions, process count = ${sessionIdsToProcess.size}")
// val start = this.start
// val end = this.end
val sessions = sessionIdsToProcess.mapNotNull { realm.findById<Session>(it) }
for (session in sessions) {
// Session Sets
val startDate = session.startDate
val endDate = session.endDate
if (startDate != null && endDate != null) {
updateTimeline(session)
} else if (session.sessionSet != null) {
removeFromTimeline(session)
}
}
// FlatTimeIntervals
processFlatTimeInterval(realm, sessions.toSet(), start, end)
val ftis = realm.where(FlatTimeInterval::class.java).findAll()
Timber.d("*** FTIs count = ${ftis.size}")
}
/**
* Updates the global timeline using the updated [session]
*/
fun updateTimeline(session: Session) {
// if (!session.realm.isInTransaction) {
// throw PAIllegalStateException("realm should be in transaction at this point")
// }
if (session.startDate == null) {
throw ModelException("Start date should never be null here")
}
if (session.endDate == null) {
throw ModelException("End date should never be null here")
}
val start = session.startDate!!
val end = session.endDate!!
val sessionSets = this.matchingData<SessionSet>(session.realm, start, end)
cleanupSessionSets(session, sessionSets)
}
// private fun matchingSets(session: Session): RealmResults<SessionSet> {
// val realm = session.realm
// val endDate = session.endDate!! // tested above
// val startDate = session.startDate!!
//
// val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
//
// query
// .lessThanOrEqualTo("startDate", startDate)
// .greaterThanOrEqualTo("endDate", startDate)
// .or()
// .lessThanOrEqualTo("startDate", endDate)
// .greaterThanOrEqualTo("endDate", endDate)
// .or()
// .greaterThanOrEqualTo("startDate", startDate)
// .lessThanOrEqualTo("endDate", endDate)
//
// return query.findAll()
// }
private inline fun <reified T : RealmModel> matchingData(realm: Realm, startDate: Date, endDate: Date): RealmResults<T> {
val query: RealmQuery<T> = realm.where(T::class.java)
query
.lessThanOrEqualTo("startDate", startDate)
.greaterThanOrEqualTo("endDate", startDate)
.or()
.lessThanOrEqualTo("startDate", endDate)
.greaterThanOrEqualTo("endDate", endDate)
.or()
.greaterThanOrEqualTo("startDate", startDate)
.lessThanOrEqualTo("endDate", endDate)
return query.findAll()
}
/**
* Multiple session sets update:
* Merges or splits session sets
* Does that by deleting then recreating
*/
private fun cleanupSessionSets(session: Session, sessionSets: RealmResults<SessionSet>) {
// get all endedSessions from sets
val allImpactedSessions = mutableSetOf<Session>()
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { allImpactedSessions.addAll(it) }
}
allImpactedSessions.add(session)
// delete all sets
sessionSets.deleteAllFromRealm()
allImpactedSessions.forEach { impactedSession ->
val sets = matchingData<SessionSet>(impactedSession.realm, impactedSession.startDate!!, impactedSession.endDate!!)
this.updateTimeFrames(sets, impactedSession)
}
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
}
/**
* Update the global timeline using the impacted [sessionSets] and the updated [session]
*/
private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, session: Session) {
when (sessionSets.size) {
0 -> this.createOrUpdateSessionSet(session)
else -> this.mergeSessionGroups(session, sessionSets)
}
}
/**
* Creates or update the session set for the [session]
*/
private fun createOrUpdateSessionSet(session: Session) {
val set = session.sessionSet
if (set != null) {
set.startDate = session.startDate!! // tested above
set.endDate = session.endDate!!
} else {
this.createSessionSet(session)
}
}
/**
* Create a set and affect it to the [session]
*/
private fun createSessionSet(session: Session) {
val set = SessionSet.newInstance(session.realm)
set.startDate = session.startDate!!
set.endDate = session.endDate!!
set.breakDuration = session.breakDuration
session.sessionSet = set
set.computeStats()
}
/**
* Multiple session sets update:
* Merges all sets into one (delete all then create a new one)
*/
private fun mergeSessionGroups(session: Session, sessionSets: RealmResults<SessionSet>) {
var startDate = session.startDate!!
var endDate = session.endDate!!
// get all endedSessions from sets
val sessions = mutableSetOf<Session>()
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { sessions.addAll(it) }
}
// find earlier and later dates from all sets
sessions.forEach { s ->
if (s.startDate != null && s.endDate != null) {
val start = s.startDate!!
val end = s.endDate!!
if (start.before(startDate)) {
startDate = start
}
if (end.after(endDate)) {
endDate = end
}
} else {
throw CorruptSessionSetException("Set contains unfinished sessions!")
}
}
// delete all sets
sessionSets.deleteAllFromRealm()
// Create a new set
val set = SessionSet.newInstance(session.realm)
set.startDate = startDate
set.endDate = endDate
// Add the session linked to this timeframe to the new sessionGroup
session.sessionSet = set
// Add all orphan endedSessions
sessions.forEach { s ->
s.sessionSet = set
}
set.computeStats()
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
}
/**
* Removes the [session] from the timeline
*/
fun removeFromTimeline(session: Session) {
// if (!session.realm.isInTransaction) {
// throw PAIllegalStateException("realm should be in transaction at this point")
// }
val sessionSet = session.sessionSet
if (sessionSet != null) {
val sessions = mutableSetOf<Session>()
sessionSet.sessions?.asIterable()?.let { sessions.addAll(it) }
sessions.remove(session)
sessionSet.deleteFromRealm()
sessions.forEach {
updateTimeline(it)
}
}
}
private fun processFlatTimeInterval(realm: Realm, changedSessions: Set<Session>, start: Date, end: Date) {
// Timber.d("***************************************************")
// Timber.d("*** processFlatTimeInterval, from: $start, to $end")
// Timber.d("***************************************************")
val sessions = matchingData<Session>(realm, start, end)
val intervalsStore = IntervalsStore(sessions.toSet())
intervalsStore.processSessions(changedSessions)
Timber.d("*** sessions count = ${intervalsStore.sessions.size}")
Timber.d("*** ftis to delete: ${intervalsStore.intervals.size}")
for (fti in intervalsStore.intervals) {
fti.deleteFromRealm()
}
// intervalsStore.intervals.forEach { it.deleteFromRealm() }
val intervals = SessionInterval.intervalMap(intervalsStore.sessions)
for (interval in intervals) {
val sortedDates = interval.dates.sorted()
for (i in (0 until sortedDates.size - 1)) {
val s = sortedDates[i]
val e = sortedDates[i + 1]
val matchingSessions = interval.sessions.filter {
val sd = it.startDate
val ed = it.endDate
(sd != null && ed != null && sd <= s && ed >= e)
}
if (matchingSessions.isNotEmpty()) {
// Timber.d("**** Create FTI: $s - $e")
val fti = FlatTimeInterval()
fti.startDate = s
fti.endDate = e
for (session in matchingSessions) {
session.flatTimeIntervals.add(fti)
realm.insertOrUpdate(session)
}
realm.insertOrUpdate(fti)
} else {
Timber.w("The FTI has no sessions")
}
}
}
sessions.forEach {
Timber.d("ending process...session FTI count = ${it.flatTimeIntervals.size}")
}
}
}
class IntervalsStore(sessionSet: Set<Session>) {
var start: Date = Date()
var end: Date = Date(0L)
val intervals = mutableSetOf<FlatTimeInterval>()
val sessions = mutableSetOf<Session>()
private val sessionIds: MutableSet<String> = mutableSetOf()
init {
processSessions(sessionSet)
}
fun processSessions(sessions: Set<Session>) {
this.sessions.addAll(sessions)
for (session in sessions) {
// Timber.d("PROCESS > s = ${session.startDate} / e = ${session.endDate} ")
loadIntervals(session)
}
}
private fun loadIntervals(session: Session) {
if (sessionIds.contains(session.id)) {
return
}
session.startDate?.let { this.start = min(this.start, it) }
session.endDate?.let { this.end = max(this.end, it) }
this.sessionIds.add(session.id)
Timber.d("session FTI count = ${session.flatTimeIntervals.size}")
for (fti in session.flatTimeIntervals) {
this.intervals.add(fti)
fti.sessions?.let { sessions ->
processSessions(sessions.toSet())
}
}
}
}
class SessionInterval(session: Session) {
var start: Date
var end: Date?
var sessions: MutableSet<Session> = mutableSetOf()
val dates: MutableSet<Date> = mutableSetOf()
val duration: Long
get() {
val endDate = end ?: Date()
return endDate.time - start.time
}
init {
this.start = session.startDate!!
this.end = session.endDate
// Timber.d("INTERVAL init: s = $start, e = $end")
this.addSession(session)
}
private fun addSession(session: Session) {
this.sessions.add(session)
session.startDate?.let { this.dates.add(it) }
session.endDate?.let { endDate ->
this.dates.add(endDate)
if (endDate > end) {
end = endDate
}
}
}
companion object {
fun intervalMap(sessions: Set<Session>): List<SessionInterval> {
val sorted = sessions.sortedBy { it.startDate }
val intervals = mutableListOf<SessionInterval>()
sorted.firstOrNull()?.let { firstSession ->
var currentInterval = SessionInterval(firstSession)
intervals.add(currentInterval)
val remainingSessions = sorted.drop(1)
for (session in remainingSessions) {
val start = session.startDate!!
val currentEnd = currentInterval.end
if (currentEnd != null && start > currentEnd) {
val interval = SessionInterval(session)
currentInterval = interval
intervals.add(interval)
} else {
currentInterval.addSession(session)
}
}
}
// intervals.forEach {
// Timber.d("s = ${it.start}, e = ${it.end}")
// }
return intervals
}
}
}

@ -0,0 +1,99 @@
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()
})
}
}

@ -17,8 +17,10 @@ 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.*
@ -76,6 +78,7 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed()
lookForCalendarBadge()
checkForFailedBackups()
}
private lateinit var binding: ActivityHomeBinding
@ -113,11 +116,11 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
val realm = getRealm()
// observe currency changes
this.currencies = realm.where(Currency::class.java).findAllAsync()
this.currencies.addChangeListener { _, _ ->
getRealm().executeTransactionAsync { asyncRealm ->
asyncRealm.where(Currency::class.java).findAll().forEach { currency ->
currency.refreshRelatedRatedValues()
this.currencies = realm.where(Currency::class.java).findAll()
this.currencies.addChangeListener { currencies, _ ->
realm.executeTransaction {
currencies.forEach {
it.refreshRelatedRatedValues()
}
}
}
@ -205,4 +208,22 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
}
private fun checkForFailedBackups() {
if (!Preferences.sessionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.SESSION, email, this)
task.start()
}
}
if (!Preferences.transactionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.TRANSACTION, email, this)
task.start()
}
}
}
}

@ -18,7 +18,6 @@ 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() {
@ -64,11 +63,13 @@ class ImportActivity : BaseActivity() {
val fragmentTransaction = supportFragmentManager.beginTransaction()
val fragment = ImportFragment()
val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis")
fis?.let {
fragment.setData(it)
}
fragment.setData(fileURI)
// 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()

@ -3,6 +3,7 @@ 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
@ -21,6 +22,8 @@ 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
@ -34,7 +37,6 @@ abstract class BaseActivity : AppCompatActivity() {
}
private var realm: Realm? = null
private var permissionCallback: ((granted: Boolean) -> Unit)? = null
private var permissionRequest: PermissionRequest? = null
@ -56,12 +58,14 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState")
setLanguage()
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState")
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash
setLanguage()
}
override fun onResume() {
@ -87,7 +91,7 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onBackPressed() {
super.onBackPressed()
AppReviewManager.showReviewManagerIfNecessary(this)
AppReviewManager.showReviewManager(this)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
@ -130,19 +134,31 @@ abstract class BaseActivity : AppCompatActivity() {
fragmentTransaction.commit()
}
fun addFragmentWithBackStack(fragment: Fragment, containerId: Int) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(containerId, fragment)
fragmentTransaction.addToBackStack(fragment.javaClass.toString())
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
*/
fun getRealm(): Realm {
// return this.realm
this.realm?.let {
return it
} ?: run {
@ -152,23 +168,6 @@ abstract class BaseActivity : AppCompatActivity() {
}
}
fun executeRealmAsyncTransaction(handler: (Realm) -> (Unit)) {
this.paApplication.executeRealmAsyncTransaction(handler)
// Timber.d(">>>> Launch async transaction")
//
// this.realm.executeTransactionAsync({ asyncRealm ->
// handler(asyncRealm)
// }, {
// Timber.d("YEAAAAAAAAAAAH !!!")
// this.realm.refresh()
// }, {
// Timber.d("NOOOOO error = $it")
// })
}
/**
* Return if the location permission has been granted by the user
*/

@ -0,0 +1,187 @@
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,7 +16,8 @@ enum class RequestCode(var value: Int) {
IMPORT(900),
SUBSCRIPTION(901),
CURRENCY(902),
PERMISSION_WRITE_EXTERNAL_STORAGE(1000)
PERMISSION_WRITE_EXTERNAL_STORAGE(1000),
CAMERA(1001)
}
enum class ResultCode(var value: Int) {

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

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

@ -163,14 +163,26 @@ fun showEditTextAlertDialog(
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))
editText.inputType = inputType
builder.setView(editText)
layout.addView(editText, params)
builder.setView(layout)
// builder.setView(editText)
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
positiveAction?.invoke(editText.text.toString())

@ -45,27 +45,46 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
)
}
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 =
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 class CurrencyRow(var currency: Currency) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return currency.getDisplayName(Locale.getDefault()).capitalize()
return this.currency.getDisplayName(Locale.getDefault()).capitalize()
}
var currencyCode: String = currency.currencyCode
var currencySymbol: String = currency.getSymbol(Locale.getDefault())
var currencyCode: String = this.currency.currencyCode
var currencySymbol: String = this.currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
@ -110,9 +129,6 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
// RowRepresentableDelegate
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
val intent = Intent()
intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode)
this.activity?.setResult(Activity.RESULT_OK, intent)

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

@ -1,34 +1,32 @@
package net.pokeranalytics.android.ui.fragment
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.*
import net.pokeranalytics.android.AppState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentImportBinding
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate
import net.pokeranalytics.android.util.csv.ImportException
import timber.log.Timber
import java.io.InputStream
import java.text.NumberFormat
import java.util.*
enum class ImportBroadcast(var identifier: String) {
START("start-import"),
END("end-import")
}
class ImportFragment : RealmFragment(), ImportDelegate {
private lateinit var filePath: String
private lateinit var inputStream: InputStream
private lateinit var uri: Uri
private lateinit var importer: CSVImporter
private var _binding: FragmentImportBinding? = null
@ -57,6 +55,10 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.inputStream = inputStream
}
fun setData(uri: Uri) {
this.uri = uri
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -89,21 +91,22 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private fun startImport() {
AppState.isImporting = true
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(Intent(ImportBroadcast.START.identifier))
this.parentActivity?.paApplication?.reportWhistleBlower?.pause()
// this.parentActivity?.paApplication?.reportWhistleBlower?.pause()
this.importer = CSVImporter(inputStream)
this.importer = CSVImporter(uri, requireContext())
this.importer.delegate = this
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(coroutineContext).launch {
val coroutine = CoroutineScope(Dispatchers.Default).async {
val coroutine = GlobalScope.async {
val s = Date()
Timber.d(">>> Start Import...")
importer.start()
try {
importer.start()
} catch (e: ImportException) {
exceptions.add(e)
}
val e = Date()
val duration = (e.time - s.time) / 1000.0
@ -139,10 +142,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
}
private fun end() {
AppState.isImporting = false
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(Intent(ImportBroadcast.END.identifier))
// this.parentActivity?.paApplication?.reportWhistleBlower?.resume()
this.parentActivity?.paApplication?.reportWhistleBlower?.resume()
activity?.finish()
}

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

@ -1,6 +1,8 @@
package net.pokeranalytics.android.ui.fragment
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
@ -8,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import io.realm.RealmResults
@ -18,6 +21,8 @@ import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -26,6 +31,7 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -41,6 +47,7 @@ import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences
import timber.log.Timber
import java.util.Date
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
@ -59,7 +66,10 @@ data class ReportSection(val report: StaticReport, var performances: MutableList
}
data class PerformanceRow(val performance: Performance, val report: StaticReport): RowRepresentable {
override val resId: Int? = this.performance.resId
override val viewType: Int = RowViewType.TITLE_BADGE_VALUE.identifier
}
@ -67,7 +77,6 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
private lateinit var reportSetups: RealmResults<ReportSetup>
private lateinit var performances: RealmResults<Performance>
private var adapterRows = mutableListOf<RowRepresentable>()
override fun deletableItems(): List<Deletable> {
@ -128,7 +137,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName)
itemToDeleteId?.let { id ->
CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(coroutineContext).launch {
delay(300)
deleteItem(dataListAdapter, reportSetups, id)
}
@ -149,21 +158,15 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
* Init data
*/
private fun initData() {
this.reportSetups = getRealm().where(ReportSetup::class.java).sort("name").findAllAsync()
this.performances = getRealm().where(Performance::class.java).findAllAsync()
this.reportSetups = getRealm().where(ReportSetup::class.java).findAll().sort("name")
this.reportSetups.addChangeListener { _, _ ->
if (isAdded) {
this.updateRows()
}
this.updateRows()
}
this.performances = getRealm().where(Performance::class.java).findAll()
this.performances.addChangeListener { _, _ ->
if (isAdded) {
this.updateRows()
}
this.updateRows()
}
}
/**
@ -185,10 +188,24 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
adapter = dataListAdapter
}
binding.addButton.setOnClickListener {
binding.addButton.setOnClickListener {
ReportCreationActivity.newInstanceForResult(this, requireContext())
}
val sessionCount = getRealm().where(Result::class.java).count()
binding.computeButton.isVisible = adapterRows.isEmpty() && sessionCount > 5
binding.computeButton.setOnClickListener {
try {
forceReportWhistleBlowerStart()
} catch (e: Exception) {
e.message?.let {
this.showSnackBar(it)
}
}
}
this.paApplication?.reportWhistleBlower?.addListener(this)
}
@ -202,7 +219,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
private fun updateRows() {
this.adapterRows.clear()
if (this.reportSetups.size > 0) {
if (this.reportSetups.isNotEmpty()) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups)
}
@ -294,7 +311,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
*/
private fun launchComputation(criteriaList: List<Criteria>, reportName: String, stat: Stat) {
if (criteriaList.combined(getRealm()).size < 2) {
if (criteriaList.combined().size < 2) {
Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show()
return
}
@ -314,17 +331,19 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
*/
private fun launchReportWithOptions(options: Calculator.Options, reportDisplay: ReportDisplay, reportName: String) {
Timber.d("launchReportWithOptions")
showLoader()
CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(coroutineContext).launch {
val startDate = Date()
val realm = Realm.getDefaultInstance()
realm.refresh()
val report = Calculator.computeStats(realm, options = options)
CoroutineScope(Dispatchers.Main).launch {
Timber.d("launchComputation: ${System.currentTimeMillis() - startDate.time}ms")
launch(Dispatchers.Main) {
if (!isDetached) {
hideLoader()
ReportActivity.newInstanceForResult(this@ReportsFragment, report, reportDisplay, reportName)
@ -343,4 +362,32 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
}
private fun forceReportWhistleBlowerStart() {
val rwb = ReportWhistleBlower(requireContext())
val reportTask = ReportTask(rwb, requireContext())
reportTask.handler = {
Timber.d("test")
val paused = paApplication?.reportWhistleBlower?.paused
reportTask.messages.add(">>> main RWB paused = $paused")
val message = reportTask.messages.joinToString("\n")
CoroutineScope(coroutineContext).launch(Dispatchers.Main) {
Timber.d("test2")
copyToClipboard(requireContext(), message)
}
}
reportTask.start()
}
fun copyToClipboard(context: Context, text: String) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("label", text)
clipboard.setPrimaryClip(clip)
}
}

@ -11,9 +11,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm
import net.pokeranalytics.android.BuildConfig
@ -42,7 +45,12 @@ import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.FileUtils
import net.pokeranalytics.android.util.Language
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.StopNotificationManager
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener
@ -51,7 +59,7 @@ import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.*
import java.util.Date
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -68,11 +76,12 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
return fragment
}
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(SettingsRow.getRows())
rows
}
// fun rowRepresentation(context: Context): List<RowRepresentable> {
// val rows = ArrayList<RowRepresentable>()
// val hasBackupEmail = Preferences.getBackupEmail(context)?.isNotBlank() ?: false
// rows.addAll(SettingsRow.getRows(hasBackupEmail))
// return rows
// }
}
@ -156,27 +165,27 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
private fun updateMainCurrency(currencyCode: String) {
Preferences.getDefaultCurrency(requireContext())?.currencyCode?.let { mainCurrencyCode ->
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
val mainCurrencyCode = UserDefaults.currency.currencyCode
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ ->
hideLoader()
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
val message = requireContext().getString(R.string.currency_rate_confirmation, mainCurrencyCode, currencyCode)
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ ->
hideLoader()
val message = requireContext().getString(R.string.currency_rate_confirmation, mainCurrencyCode, currencyCode)
// val message = "Please enter the $mainCurrencyCode to $currencyCode rate to apply to all your bankrolls"
showEditTextAlertDialog(requireContext(), InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL,
context?.let { context ->
showEditTextAlertDialog(context, InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL,
message = message, editTextText = apiRate?.toString()) { value ->
value.toDoubleOrNull()?.let { rate ->
updateMainCurrency(currencyCode, rate)
}
}
}
}
}
@ -185,12 +194,12 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance()
executeRealmAsyncTransaction { execRealm->
execRealm.where(Currency::class.java).findAll().forEach { currency ->
realm.executeTransaction {
realm.where(Currency::class.java).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate
}
execRealm.where(Session::class.java).findAll().forEach { session ->
realm.where(Session::class.java).findAll().forEach { session ->
session.bankrollHasBeenUpdated()
}
}
@ -199,7 +208,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
}
override fun adapterRows(): List<RowRepresentable> {
return rowRepresentation
return SettingsRow.getRows()
}
override fun charSequenceForRow(
@ -211,6 +220,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext())
SettingsRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else ""
SettingsRow.CURRENCY -> UserDefaults.currency.symbol
SettingsRow.BACKUP_EMAIL -> Preferences.getBackupEmail(requireContext()) ?: ""
else -> ""
}
}
@ -220,6 +230,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext())
SettingsRow.SHOULD_SHOW_BLOG_TIPS -> Preferences.shouldShowBlogTips(requireContext())
SettingsRow.SHOW_INAPP_BADGES -> Preferences.showInAppBadges(requireContext())
SettingsRow.BACKUP_EMAIL -> !Preferences.hasBackupEmail(requireContext())
else -> false
}
}
@ -236,11 +247,13 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
this.openPlayStoreAccount()
}
}
SettingsRow.LANGUAGE -> this.showLanguagePopup()
SettingsRow.RATE_APP -> showReviewManager()
SettingsRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact)
SettingsRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingsRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingsRow.DEALT_HANDS_PER_HOUR -> DealtHandsPerHourActivity.newInstance(requireContext())
SettingsRow.BACKUP_EMAIL -> this.editBackupEmail()
SettingsRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport()
SettingsRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport()
SettingsRow.FOLLOW_US -> {
@ -264,6 +277,40 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
}
}
private fun showLanguagePopup() {
context?.let { context ->
val languages = Language.values()
val labels = Language.values().map { it.dualNames }
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
builder.setTitle(R.string.language_popup_message)
builder.setItems(labels.toTypedArray()) { _, which ->
// the user clicked on colors[which]
val locale = languages[which]
Preferences.setLanguageCode(locale.code, context)
this.view?.let { view ->
val snackBar = Snackbar.make(view, R.string.language_should_restart_app_popup_message, Snackbar.LENGTH_INDEFINITE)
snackBar.show()
}
}
builder.show()
}
}
private fun editBackupEmail() {
context?.let { context ->
showEditTextAlertDialog(context, InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, messageResId = R.string.backup_email_title, editTextText = Preferences.getBackupEmail(context)) { value ->
Preferences.setBackupEmail(value, context)
this.settingsAdapterRow.notifyDataSetChanged()
}
}
}
private fun showReviewManager() {
val manager = ReviewManagerFactory.create(requireContext())
@ -277,6 +324,8 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed
}
} else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result.
}
}

@ -8,9 +8,8 @@ import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.Toolbar
import io.realm.Realm
import io.realm.RealmModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
@ -74,10 +73,9 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
this.currentFilterable = FilterableType.SESSION
applyFilter()
addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java)
addRealmChangeListener(this, SessionSet::class.java)
addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java)
}
private fun initUI() {
@ -101,11 +99,10 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
private fun setTransactionFilterItemColor() {
context?.let {
UserConfig.getConfiguration(getRealm()) { userConfig ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
val userConfig = UserConfig.getConfiguration(getRealm())
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
}
}
@ -149,13 +146,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
// Business
override fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>) {
override fun asyncListenedEntityChange(realm: Realm) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchStatComputation()
setTransactionFilterItemColor()
}
}
/**
@ -163,9 +158,9 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/
private fun launchStatComputation() {
CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(coroutineContext).launch {
val async = async {
val async = GlobalScope.async {
val s = Date()
// Timber.d(">>> start...")
@ -179,15 +174,13 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val e = Date()
val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> computations took $duration seconds")
// Timber.d(">>> ended in $duration seconds")
}
async.await()
launch(Dispatchers.Main) {
if (isAdded && !isDetached) {
tableReportFragment.showResults()
}
if (isAdded && !isDetached) {
tableReportFragment.showResults()
}
}
}
@ -197,10 +190,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/
private fun createSessionGroupsAndStartCompute(realm: Realm): Report {
// Timber.d(">>> Launch statistics computations")
val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let {
if (it.filterableType == currentFilterable) { it } else { null }
var filter: Filter? = null
context?.let { context ->
this.currentFilter(context, realm)?.let { current ->
if (current.filterableType == currentFilterable) { filter = current }
}
}
val allStats: List<Stat> = listOf(
@ -210,7 +204,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
Stat.NUMBER_OF_SETS,
Stat.AVERAGE_HOURLY_DURATION,
Stat.HOURLY_DURATION,
Stat.FTI_COUNT,
Stat.HANDS_PLAYED
)
@ -248,6 +241,8 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats)
// Timber.d(">>>>> Start computations...")
val options = Calculator.Options()
val computedStats = mutableListOf<Stat>()
computedStats.addAll(allStats)
@ -255,9 +250,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
computedStats.addAll(tStats)
options.stats = computedStats
UserConfig.getConfiguration(realm) { userConfig ->
options.includedTransactions = userConfig.transactionTypes(realm)
}
options.includedTransactions = UserConfig.getConfiguration(realm).transactionTypes(realm)
return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options)
}

@ -30,6 +30,7 @@ import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import timber.log.Timber
import java.lang.ref.WeakReference
import java.time.Period
@ -75,7 +76,7 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
AppGuard.registerListener(this)
if (!isNetworkAvailable()) {
if (!requireContext().isNetworkAvailable()) {
Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show()
return
}
@ -158,10 +159,10 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
purchase.isEnabled = true
purchase.setOnClickListener {
val network = isNetworkAvailable()
val network = requireContext().isNetworkAvailable()
Timber.d("isNetworkAvailable = $network ")
if (!isNetworkAvailable()) {
if (!network) {
Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show()
return@setOnClickListener
}

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

@ -1,11 +1,10 @@
package net.pokeranalytics.android.ui.fragment.components
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import net.pokeranalytics.android.PokerAnalyticsApplication
@ -42,6 +41,12 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
v.setPadding(0, statusBarHeight, 0, 0)
insets
}
}
override fun onResume() {
@ -109,11 +114,6 @@ abstract class BaseFragment : Fragment() {
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
parentActivity?.setSupportActionBar(toolbar)
}
context?.getColor(R.color.kaki_darkest)?.let { color ->
view?.setBackgroundColor(color)
}
}
/**
@ -181,15 +181,6 @@ abstract class BaseFragment : Fragment() {
alternativeLabels)
}
/***
* Returns whether the network is available or not
*/
fun isNetworkAvailable(): Boolean {
val cm = requireContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capability = cm.getNetworkCapabilities(cm.activeNetwork)
return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
}
fun showSnackBar(message: String) {
this.view?.let { view ->
val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE)

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

@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.view.*
import android.widget.ImageView
@ -57,11 +58,12 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
parentActivity?.registerReceiver(
updateFilterUIBroadcast, IntentFilter(
INTENT_FILTER_UPDATE_FILTER_UI
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI), Context.RECEIVER_EXPORTED)
} else {
parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI))
}
}
override fun onDestroy() {

@ -1,45 +1,39 @@
package net.pokeranalytics.android.ui.fragment.components
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmResults
import net.pokeranalytics.android.AppState
import kotlinx.coroutines.Dispatchers
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.fragment.ImportBroadcast
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
interface RealmAsyncListener {
fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>)
fun asyncListenedEntityChange(realm: Realm)
}
open class RealmFragment : BaseFragment() {
val coroutineContext: CoroutineContext
get() = Dispatchers.Main
/**
* A realm instance
*/
// private lateinit var realm: Realm
private lateinit var realm: Realm
/***
* A listener to async updates
*/
private var changeListener: RealmAsyncListener? = null
private val endImportReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
sendRealmChange()
}
}
// private var realmResults: RealmResults<out RealmModel>? = null
private var realmResults: RealmResults<out RealmModel>? = null
/**
* A List of observed RealmResults
@ -47,8 +41,18 @@ open class RealmFragment : BaseFragment() {
private var observedRealmResults: MutableList<RealmResults<*>> = mutableListOf()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// realm = Realm.getDefaultInstance()
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(endImportReceiver, IntentFilter(ImportBroadcast.END.identifier))
realm = Realm.getDefaultInstance()
this.observedEntities.forEach {
val realmResults = realm.where(it).findAll()
realmResults.addChangeListener { t, _ ->
this.entitiesChanged(it, t)
}
this.observedRealmResults.add(realmResults)
}
return super.onCreateView(inflater, container, savedInstanceState)
}
@ -56,11 +60,7 @@ open class RealmFragment : BaseFragment() {
* Get the realm instance
*/
fun getRealm(): Realm {
return this.parentActivity?.getRealm() ?: throw PAIllegalStateException("parent activity missing")
}
fun executeRealmAsyncTransaction(handler: (Realm) -> (Unit)) {
this.parentActivity?.executeRealmAsyncTransaction(handler) ?: throw PAIllegalStateException("parent activity missing")
return this.realm
}
fun addRealmChangeListener(listener: RealmAsyncListener, clazz: Class<out RealmModel>) {
@ -70,48 +70,33 @@ open class RealmFragment : BaseFragment() {
}
this.changeListener = listener
val results = getRealm().where(clazz).findAllAsync()
results.addChangeListener { res, _ ->
// Timber.d("Realm changes: ${realmResults?.size}, $this")
if (!AppState.isImporting) {
this.changeListener?.asyncListenedEntityChange(res.realm, clazz)
} else {
this.changedClasses.add(clazz)
}
val results = this.realm.where(clazz).findAllAsync()
results.addChangeListener { t, _ ->
Timber.d("Realm changes: ${realmResults?.size}, $this")
this.changeListener?.asyncListenedEntityChange(t.realm)
}
this.observedRealmResults.add(results)
}
private var changedClasses: MutableSet<Class<out RealmModel>> = mutableSetOf()
fun sendRealmChange() {
if (!AppState.isImporting) {
for (clazz in this.changedClasses) {
this.changeListener?.asyncListenedEntityChange(getRealm(), clazz)
}
this.changedClasses.clear()
}
}
override fun onDestroyView() {
super.onDestroyView()
LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(endImportReceiver)
this.observedRealmResults.forEach {
it.removeAllChangeListeners()
}
this.realm.close()
this.realmResults?.removeAllChangeListeners()
}
/**
* A list of RealmModel classes to observe
*/
// open val observedEntities: List<Class<out RealmModel>> = listOf()
open val observedEntities: List<Class<out RealmModel>> = listOf()
/**
* The method called when a change happened in any RealmResults
*/
// open fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {}
open fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {}
}

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

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

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

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

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

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

@ -3,11 +3,9 @@ package net.pokeranalytics.android.ui.fragment.report
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
@ -60,8 +58,8 @@ abstract class AbstractReportFragment : DataManagerFragment() {
override fun saveData() {
activity?.let {
val builder = AlertDialog.Builder(it)
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
// Get the layout inflater
val inflater = requireActivity().layoutInflater
@ -94,19 +92,6 @@ abstract class AbstractReportFragment : DataManagerFragment() {
}
val dialog = builder.create()
dialog.setOnShowListener {
nameEditText.requestFocus()
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
dialog.setOnDismissListener {
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
dialog.show()
} ?: throw PAIllegalStateException("Activity cannot be null")
@ -121,12 +106,9 @@ abstract class AbstractReportFragment : DataManagerFragment() {
this.reportViewModel.title = name
val rs = this.model.item as ReportSetup
val setupId = rs.id
val firstSave = (this.model.primaryKey == null)
executeRealmAsyncTransaction { realm ->
getRealm().executeTransaction { realm ->
val firstSave = (this.model.primaryKey == null)
if (firstSave) {
val options = this.selectedReport.options
rs.name = name
@ -144,18 +126,15 @@ abstract class AbstractReportFragment : DataManagerFragment() {
options.filterId?.let { id ->
rs.filter = realm.findById(id)
}
realm.insertOrUpdate(rs)
// realm.copyToRealmOrUpdate(rs)
realm.copyToRealmOrUpdate(rs)
} else {
realm.findById<ReportSetup>(setupId)?.let { reportSetup ->
reportSetup.name = name
realm.insertOrUpdate(reportSetup)
}
rs.name = name
realm.insertOrUpdate(rs)
}
}
this.model.primaryKey = setupId
this.model.primaryKey = rs.id
this.deleteButtonShouldAppear = true
setToolbarTitle(this.reportViewModel.title)
}

@ -5,9 +5,14 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
@ -28,10 +33,14 @@ import net.pokeranalytics.android.ui.view.rows.StatRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import timber.log.Timber
import java.util.*
open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource,
open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, CoroutineScope,
RowRepresentableDelegate {
// override val coroutineContext: CoroutineContext
// get() = Dispatchers.Main
companion object {
/**
@ -75,6 +84,11 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
initData()
initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
report?.let {
showResults()
}
@ -149,25 +163,17 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
private fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
val rows: ArrayList<RowRepresentable> = ArrayList()
report.results.forEach { result ->
val title = result.group.query.getName(requireContext()).capitalize()
rows.add(CustomizableRowRepresentable(title = title))
val statList = result.group.displayedStats ?: report.options.stats
statList.forEach { stat ->
rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(requireContext())))
this.context?.let { context ->
report.results.forEach { result ->
val title = result.group.query.getName(context).capitalize()
rows.add(CustomizableRowRepresentable(title = title))
val statList = result.group.displayedStats ?: report.options.stats
statList.forEach { stat ->
rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(context)))
}
}
}
return rows
// val rows: ArrayList<RowRepresentable> = ArrayList()
// report.options.stats.forEach {stat ->
// rows.add(CustomizableRowRepresentable(title = stat.localizedTitle(requireContext())))
// report.results.forEach {
// val title = it.group.name
// rows.add(StatRow(stat, it.computedStat(stat), it.group.name, title))
// }
// }
// return rows
}
// RowRepresentableDelegate
@ -204,11 +210,11 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
showLoader()
CoroutineScope(Dispatchers.Default).launch {
GlobalScope.launch(coroutineContext) {
var report: Report? = null
val test = async {
// val s = Date()
val test = GlobalScope.async {
val s = Date()
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
@ -220,9 +226,9 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
realm.close()
// val e = Date()
// val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> ended in $duration seconds")
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
}
test.await()

@ -11,16 +11,15 @@ import com.github.mikephil.charting.data.LineDataSet
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.*
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.*
import net.pokeranalytics.android.databinding.FragmentProgressReportBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.combined
@ -83,12 +82,6 @@ class ProgressReportFragment : AbstractReportFragment() {
override fun onDestroyView() {
// childFragmentManager.findFragmentByTag(GRAPH_TAG)?.let { fragment ->
// val fragmentTransaction = childFragmentManager.beginTransaction()
// fragmentTransaction.remove(fragment)
// fragmentTransaction.commit()
// }
AppReviewManager.requestReview()
super.onDestroyView()
@ -141,7 +134,7 @@ class ProgressReportFragment : AbstractReportFragment() {
when (aggregationType) {
AggregationType.MONTH, AggregationType.YEAR -> {
if (aggregationType.criterias.combined(getRealm()).size < 2) {
if (aggregationType.criterias.combined().size < 2) {
Toast.makeText(context, R.string.less_then_2_values_for_display, Toast.LENGTH_LONG).show()
return
}
@ -170,29 +163,30 @@ class ProgressReportFragment : AbstractReportFragment() {
graphContainer.hideWithAnimation()
progressBar.showWithAnimation()
CoroutineScope(Dispatchers.Default).launch {
GlobalScope.launch {
// val s = Date()
val s = Date()
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
val group = selectedReport.results.first().group
val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType)
reports[aggregationType] = report
selectedReport.results.firstOrNull()?.group?.let { group ->
val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType)
reports[aggregationType] = report
realm.close()
realm.close()
// val e = Date()
// val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> ended in $duration seconds")
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)
progressBar.hideWithAnimation()
graphContainer.showWithAnimation()
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)
progressBar.hideWithAnimation()
graphContainer.showWithAnimation()
}
}
}
}

@ -14,7 +14,7 @@ object AppReviewManager {
this.reviewRequested = true
}
fun showReviewManagerIfNecessary(activity: Activity) {
fun showReviewManager(activity: Activity) {
if (this.reviewRequested && this.shouldAskForReview(activity.baseContext)) {
this.reviewRequested = false

@ -35,6 +35,8 @@ import net.pokeranalytics.android.ui.view.rows.BankrollTotalRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.sorted
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
interface BankrollRowRepresentable : RowRepresentable {
var bankrollId: String?

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

@ -11,13 +11,11 @@ import io.realm.Realm
import io.realm.RealmModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.AppState
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.databinding.FragmentCalendarBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
@ -25,7 +23,9 @@ import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined
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.ComputableResult
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.UserConfig
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
@ -43,7 +43,8 @@ import timber.log.Timber
import java.util.*
import kotlin.collections.set
class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentableDataSource,
RowRepresentableDelegate, RealmAsyncListener {
enum class TimeFilter {
@ -86,13 +87,6 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
private var _binding: FragmentCalendarBinding? = null
private val binding get() = _binding!!
private val requiredStats: List<Stat > = listOf(
Stat.LOCATIONS_PLAYED,
Stat.LONGEST_STREAKS,
Stat.DAYS_PLAYED,
Stat.STANDARD_DEVIATION_HOURLY
)
// Life Cycle
override fun onCreateView(
@ -107,15 +101,13 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
initData()
initUI()
addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java)
addRealmChangeListener(this, SessionSet::class.java)
}
private var transactionFilterMenuItem: MenuItem? = null
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -141,10 +133,11 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
private fun setTransactionFilterItemColor() {
context?.let {
UserConfig.getConfiguration(getRealm()) { userConfig ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color))
val userConfig = UserConfig.getConfiguration(getRealm())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.let { item ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
item.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
}
}
@ -168,44 +161,12 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
}
private fun showDetails(computedResults: ComputedResults, title: String?) {
// start calculation with progress values
CoroutineScope(Dispatchers.Default).launch {
var report: Report? = null
val coroutine = async {
val realm = Realm.getDefaultInstance()
val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = computedResults.group.query,
includedTransactions = transactionTypes(realm)
)
report = Calculator.computeStats(realm, options = options)
realm.close()
}
coroutine.await()
launch(Dispatchers.Main) {
report?.results?.firstOrNull()?.let { cr ->
CalendarDetailsActivity.newInstance(
requireContext(),
cr,
sessionTypeCondition,
title
)
}
}
}
CalendarDetailsActivity.newInstance(
requireContext(),
computedResults,
sessionTypeCondition,
title
)
}
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
@ -250,6 +211,9 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
}
}
override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java)
// Business
/**
@ -384,71 +348,41 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
binding.progressBar.showWithAnimation()
binding.recyclerView.hideWithAnimation()
CoroutineScope(Dispatchers.Default).launch {
val async = async {
val s = Date()
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()
GlobalScope.launch {
launchStatComputation(realm)
val realm = Realm.getDefaultInstance()
realm.refresh()
realm.close()
launchStatComputation(realm)
val e = Date()
val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> computations took $duration seconds")
realm.close()
}
async.await()
launch(Dispatchers.Main) {
if (isAdded && !isDetached) {
displayData()
}
GlobalScope.launch(Dispatchers.Main) {
displayData()
}
}
// CoroutineScope(Dispatchers.Default).launch {
//
// val realm = Realm.getDefaultInstance()
// realm.refresh()
//
// launchStatComputation(realm)
//
// realm.close()
//
// launch(Dispatchers.Main) {
// displayData()
// }
// }
}
private fun transactionTypes(realm: Realm): List<TransactionType> {
var transactionTypes = listOf<TransactionType>()
UserConfig.getConfiguration(realm) { userConfig ->
transactionTypes = userConfig.transactionTypes(realm)
}
return transactionTypes
}
private fun launchStatComputation(realm: Realm) {
return
Timber.d(">>> Launch calendar computations")
val calendar = Calendar.getInstance()
calendar.time = Date().startOfMonth()
val startDate = Date()
val transactionTypes = this.transactionTypes(realm)
val requiredStats: List<Stat> =
listOf(
Stat.LOCATIONS_PLAYED,
Stat.LONGEST_STREAKS,
Stat.DAYS_PLAYED,
Stat.STANDARD_DEVIATION_HOURLY
)
val transactionTypes = UserConfig.getConfiguration(realm).transactionTypes(realm)
// All
val allOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = Query(this.sessionTypeCondition),
includedTransactions = transactionTypes
@ -458,6 +392,7 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
// Sliding Month [sm]
val smOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = Query(this.slidingMonthQueryCondition, this.sessionTypeCondition),
includedTransactions = transactionTypes
@ -470,24 +405,23 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
val monthlyReports: HashMap<Date, ComputedResults> = HashMap()
val monthlyQueries = when (sessionTypeCondition) {
QueryCondition.IsCash -> listOf(Criteria.AllMonthsUpToNow, Criteria.Cash).combined(realm)
QueryCondition.IsTournament -> listOf(Criteria.AllMonthsUpToNow, Criteria.Tournament).combined(realm)
else -> listOf(Criteria.Years, Criteria.MonthsOfYear).combined(realm)
QueryCondition.IsCash -> listOf(Criteria.AllMonthsUpToNow, Criteria.Cash).combined()
QueryCondition.IsTournament -> listOf(Criteria.AllMonthsUpToNow, Criteria.Tournament).combined()
else -> listOf(Criteria.Years, Criteria.MonthsOfYear).combined()
}
for (monthlyQuery in monthlyQueries) {
monthlyQueries.forEach { query ->
val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = monthlyQuery,
query = query,
includedTransactions = transactionTypes
)
val report = Calculator.computeStats(realm, options = options)
for (computedResults in report.results) {
report.results.forEach { computedResults ->
if (!computedResults.isEmpty) {
// Set date data
for (condition in monthlyQuery.conditions) {
query.conditions.forEach { condition ->
when (condition) {
is QueryCondition.AnyYear -> calendar.set(
Calendar.YEAR,
@ -504,11 +438,11 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
monthlyReports[calendar.time] = computedResults
}
}
}
// Sliding Year [sm]
val syOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = Query(this.slidingYearQueryCondition, this.sessionTypeCondition),
includedTransactions = transactionTypes
@ -523,13 +457,14 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
val yearlyReports: HashMap<Date, ComputedResults> = HashMap()
val yearConditions = when (sessionTypeCondition) {
QueryCondition.IsCash -> listOf(Criteria.Years, Criteria.Cash).combined(realm)
QueryCondition.IsTournament -> listOf(Criteria.Years, Criteria.Tournament).combined(realm)
else -> listOf(Criteria.Years).combined(realm)
QueryCondition.IsCash -> listOf(Criteria.Years, Criteria.Cash).combined()
QueryCondition.IsTournament -> listOf(Criteria.Years, Criteria.Tournament).combined()
else -> listOf(Criteria.Years).combined()
}
yearConditions.forEach { query ->
val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = query,
includedTransactions = transactionTypes
@ -563,10 +498,7 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
* Display data
*/
private fun displayData() {
// Timber.d("displayData")
this.binding.progressBar.hideWithAnimation()
this.binding.recyclerView.showWithAnimation()
Timber.d("displayData")
if (context == null) { return } // required because of launchAsyncStatComputation
@ -662,20 +594,21 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
}
}
// Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
// Timber.d("Rows: ${rows.size}")
Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
Timber.d("Rows: ${rows.size}")
this.calendarAdapter.notifyDataSetChanged()
}
this.binding.progressBar.hideWithAnimation()
this.binding.recyclerView.showWithAnimation()
override fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>) {
}
override fun asyncListenedEntityChange(realm: Realm) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchAsyncStatComputation()
setTransactionFilterItemColor()
}
}
private fun showGridCalendar() {

@ -85,6 +85,8 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
onRowValueChanged(currencyCode, BankrollPropertiesRow.CURRENCY)
if (shouldShowCurrencyRate) {
refreshRate()
} else {
this.bankroll.currency?.rate = 1.0
}
}
}

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save