Compare commits

..

44 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
  1. 139
      CLAUDE.md
  2. 41
      app/build.gradle
  3. 3
      app/src/debug/AndroidManifest.xml
  4. 85
      app/src/main/AndroidManifest.xml
  5. 47
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  6. 4
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  7. 318
      app/src/main/java/net/pokeranalytics/android/calculus/DataManager.kt
  8. 157
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  9. 2
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  10. 10
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReportManager.kt
  11. 1
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  12. 2
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  13. 17
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  14. 1
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  15. 7
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  16. 1
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  17. 2
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  18. 3
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  19. 66
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  20. 42
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  21. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  22. 18
      app/src/main/java/net/pokeranalytics/android/model/realm/Currency.kt
  23. 38
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  24. 15
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  25. 17
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  26. 9
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  27. 4
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  28. 24
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  29. 365
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  30. 14
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  31. 47
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  32. 33
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  33. 2
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  34. 19
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  35. 209
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  36. 49
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  37. 37
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  38. 3
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  39. 10
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  40. 63
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  41. 23
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  42. 10
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  43. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/Top10Fragment.kt
  44. 8
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  45. 15
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/DeletableItemFragment.kt
  46. 18
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt
  47. 5
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  48. 10
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt
  49. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStakesFragment.kt
  50. 29
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt
  51. 16
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  52. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt
  53. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/bankroll/BankrollFragment.kt
  54. 4
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt
  55. 16
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt
  56. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/GridCalendarViewModel.kt
  57. 25
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/DataManagerFragment.kt
  58. 8
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt
  59. 20
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataFragment.kt
  60. 4
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt
  61. 9
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataViewModel.kt
  62. 4
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/TransactionDataFragment.kt
  63. 12
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt
  64. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedSessionRowRepresentableAdapter.kt
  65. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt
  66. 10
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsFragment.kt
  67. 13
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterHandler.kt
  68. 19
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersFragment.kt
  69. 16
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersListFragment.kt
  70. 11
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/editor/EditorFragment.kt
  71. 24
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/ActionList.kt
  72. 87
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/EditorViewModel.kt
  73. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/FrameManager.kt
  74. 347
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt
  75. 1
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerAnimator.kt
  76. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerView.kt
  77. 8
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionActivity.kt
  78. 97
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt
  79. 16
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionViewModel.kt
  80. 22
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/DealtHandsPerHourFragment.kt
  81. 12
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/TransactionFilterFragment.kt
  82. 7
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  83. 4
      app/src/main/java/net/pokeranalytics/android/ui/view/SessionRowView.kt
  84. 76
      app/src/main/java/net/pokeranalytics/android/ui/view/keyboard/StakesKeyboardView.kt
  85. 10
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/SessionPropertiesRow.kt
  86. 34
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/BottomSheetViewModel.kt
  87. 81
      app/src/main/java/net/pokeranalytics/android/util/BackupOperator.kt
  88. 86
      app/src/main/java/net/pokeranalytics/android/util/BackupWorker.kt
  89. 24
      app/src/main/java/net/pokeranalytics/android/util/FakeDataManager.kt
  90. 16
      app/src/main/java/net/pokeranalytics/android/util/LocationManager.kt
  91. 21
      app/src/main/java/net/pokeranalytics/android/util/Preferences.kt
  92. 5
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  93. 23
      app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt
  94. 10
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  95. 23
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt
  96. 47
      app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt
  97. 3
      app/src/main/res/layout/activity_gdpr.xml
  98. 4
      app/src/main/res/layout/activity_graph.xml
  99. 2
      app/src/main/res/layout/activity_new_data.xml
  100. 1
      app/src/main/res/layout/fragment_filters.xml
  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: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' //apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android' apply plugin: 'realm-android'
// Crashlytics // Crashlytics
@ -17,7 +17,7 @@ repositories {
android { android {
compileSdkVersion 33 compileSdkVersion 35
buildToolsVersion "30.0.3" buildToolsVersion "30.0.3"
compileOptions { compileOptions {
@ -29,16 +29,13 @@ android {
jvmTarget = JavaVersion.VERSION_1_8 jvmTarget = JavaVersion.VERSION_1_8
} }
lintOptions {
disable 'MissingTranslation'
}
defaultConfig { defaultConfig {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 33 targetSdkVersion 35
versionCode 161 versionCode 180
versionName "6.0.18" versionName "6.0.38"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -91,6 +88,10 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
@ -111,13 +112,16 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews // implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places // Places
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions // Billing / Subscriptions
implementation 'com.android.billingclient:billing:5.0.0' implementation 'com.android.billingclient:billing:7.0.0'
// Import the BoM for the Firebase platform // Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -139,10 +143,10 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export) // ffmpeg for encoding video (HH export)
implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS' // implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera // Camera
def camerax_version = "1.1.0-beta01" def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}"
@ -158,14 +162,17 @@ dependencies {
// Retrofit // Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests // Instrumented Tests
androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1'
// Test // Test
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.2'
testImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2' testImplementation 'com.android.support.test:rules:1.0.2'

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

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="net.pokeranalytics.android">
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@ -84,137 +83,165 @@
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity" android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity" android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" /> android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity" android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"/> android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity" android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity" android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity" android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity" android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity" android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity" android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard" android:launchMode="standard"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity" android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity" android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity" android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity" android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity" android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity" android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity" android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity" android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog" android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop"/> android:launchMode="singleTop"
android:exported="true"/>
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity" android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> 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.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"

@ -6,13 +6,14 @@ import android.os.Build
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.DataManager import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.UserConfigObserver import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
@ -22,6 +23,7 @@ import java.util.*
class PokerAnalyticsApplication : Application() { class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null var backupOperator: BackupOperator? = null
companion object { companion object {
@ -42,11 +44,6 @@ class PokerAnalyticsApplication : Application() {
UserDefaults.init(this) UserDefaults.init(this)
if (BuildConfig.DEBUG) {
// Logs
Timber.plant(PokerAnalyticsLogs())
}
// AppGuard / Billing services // AppGuard / Billing services
AppGuard.load(this.applicationContext) AppGuard.load(this.applicationContext)
@ -54,8 +51,8 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this) Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME) .name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(15) .schemaVersion(14)
// .allowWritesOnUiThread(true) .allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
.build() .build()
@ -66,20 +63,24 @@ class PokerAnalyticsApplication : Application() {
CrashLogging.log("App onCreate. Locales = $locales") CrashLogging.log("App onCreate. Locales = $locales")
} }
if (BuildConfig.DEBUG) {
// Logs
Timber.plant(PokerAnalyticsLogs())
}
Timber.d("SDK version = ${Build.VERSION.SDK_INT}") Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
Timber.d("Realm path = ${Realm.getDefaultInstance().path}") Timber.d("Realm path = ${Realm.getDefaultInstance().path}")
// this.createFakeSessions()
} }
// Patch // Patch
Patcher.patchAll(this) Patcher.patchAll(this)
// Processors // Report
DataManager.configure(this.applicationContext) this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
UserConfigObserver.create()
// this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups // Backups
this.backupOperator = BackupOperator(this.applicationContext) this.backupOperator = BackupOperator(this.applicationContext)
@ -88,17 +89,11 @@ class PokerAnalyticsApplication : Application() {
val locale = Locale.getDefault() val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}") CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// val realm = Realm.getDefaultInstance()
// val v:Int = 0
// val set = realm.where<SessionSet>().equalTo("sessions.type", v).findAll()
// Timber.d("SESSION SET COUNT = ${set.size}")
// Realm.getDefaultInstance().executeTransaction { // Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java) // it.delete(Performance::class.java)
// } // }
// createFakeSessions()
} }
/** /**
@ -106,15 +101,15 @@ class PokerAnalyticsApplication : Application() {
*/ */
private fun createFakeSessions() { private fun createFakeSessions() {
// val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
// val sessionsCount = realm.where<Session>().count() val sessionsCount = realm.where<Session>().count()
// realm.close() realm.close()
// if (sessionsCount < 10) { if (sessionsCount < 10) {
CoroutineScope(context = Dispatchers.Default).launch { CoroutineScope(context = Dispatchers.IO).launch {
FakeDataManager.createFakeSessions(8000) FakeDataManager.createFakeSessions(500)
}
} }
// }
} }

@ -4,6 +4,7 @@ import android.content.Context
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.isNetworkAvailable import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -37,7 +38,7 @@ interface MyBackupApi {
object BackupApi { object BackupApi {
val service = BackupService() private val service = BackupService()
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send // 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 { suspend fun backupFile(context: Context, mail: String, fileName: String, fileContent: String): Boolean {
@ -60,6 +61,7 @@ object BackupApi {
true true
} catch (e: Exception) { } catch (e: Exception) {
Timber.d("!!! backup failed: ${e.message}") Timber.d("!!! backup failed: ${e.message}")
CrashLogging.logException(e)
false false
} }
} }

@ -1,318 +0,0 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import io.realm.Realm
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.Currency
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.writeAsync
import timber.log.Timber
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.
*/
object DataManager {
private var sessions: RealmResults<Session>? = null
private var currencies: RealmResults<Currency>? = null
private var dateModifiedSessionIds: MutableSet<String> = mutableSetOf()
private var statsToComputeSessionIds: MutableSet<String> = mutableSetOf()
private var changedCurrencies: MutableSet<String> = mutableSetOf()
var reportWhistleBlower: ReportWhistleBlower? = null
init {
val realm = Realm.getDefaultInstance()
sessions = realm.where(Session::class.java).findAllAsync()
sessions?.addChangeListener { sessions ->
// if (this.dateModifiedSessionIds.isNotEmpty() || this.statsToComputeSessionIds.isNotEmpty()) {
sessions.realm.writeAsync { asyncRealm ->
computeStatsIfNecessary(asyncRealm)
computeDatesIfNecessary(asyncRealm)
reportWhistleBlower?.requestReportLaunch()
}
// }
}
this.currencies = realm.where(Currency::class.java).findAll()
this.currencies?.addChangeListener { currencies, _ ->
if (changedCurrencies.isNotEmpty()) {
currencies.realm.writeAsync { asyncRealm ->
for (currencyId in this.changedCurrencies) {
asyncRealm.findById<Currency>(currencyId)?.let { currency ->
Timber.d("Compute currency ${currency.code} ")
currency.refreshRelatedRatedValues()
}
}
changedCurrencies.clear()
}
}
}
realm.close()
}
fun configure(context: Context) {
reportWhistleBlower = ReportWhistleBlower(context)
}
fun sessionToCompute(session: Session) {
// Timber.d("sessionToCompute, date = ${session.startDate} / ${session.endDate}")
statsToComputeSessionIds.add(session.id)
}
fun sessionDateChanged(session: Session) {
// Timber.d("sessionDateChanged")
dateModifiedSessionIds.add(session.id)
}
fun currencyToCompute(currency: Currency) {
// Timber.d("currencyToCompute")
changedCurrencies.add(currency.id)
}
private fun computeStatsIfNecessary(realm: Realm) {
if (statsToComputeSessionIds.isNotEmpty()) {
for (sessionId in statsToComputeSessionIds) {
realm.findById<Session>(sessionId)?.let { session ->
// Timber.d("Session Manager > compute stats, set = ${session.sessionSet}")
session.computeStats()
session.sessionSet?.computeStats()
}
}
statsToComputeSessionIds.clear()
}
}
private fun computeDatesIfNecessary(realm: Realm) {
if (dateModifiedSessionIds.isNotEmpty()) {
for (sessionId in dateModifiedSessionIds) {
realm.findById<Session>(sessionId)?.let { session ->
// Timber.d("Session Manager > manage dates, set = ${session.sessionSet}")
if (session.endDate != null) {
updateTimeline(session)
} else if (session.sessionSet != null) {
removeFromTimeline(session)
}
}
}
dateModifiedSessionIds.clear()
}
}
/**
* Updates the global timeline using the updated [session]
*/
fun updateTimeline(session: Session) {
// Timber.d("updateTimeline...")
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 = 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>) {
// Timber.d("cleanupSessionSets...")
// 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)
updateTimeFrames(sets, impactedSession)
}
}
/**
* Update the global timeline using the impacted [sessionSets] and the updated [session]
*/
private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, session: Session) {
// Timber.d("updateTimeFrames...")
when (sessionSets.size) {
0 -> createOrUpdateSessionSet(session)
else -> mergeSessionGroups(session, sessionSets)
}
}
/**
* Creates or update the session set for the [session]
*/
private fun createOrUpdateSessionSet(session: Session) {
// Timber.d("createOrUpdateSessionSet...")
val set = session.sessionSet
if (set != null) {
set.startDate = session.startDate!! // tested above
set.endDate = session.endDate!!
} else {
createSessionSet(session)
}
}
/**
* Create a set and affect it to the [session]
*/
private fun createSessionSet(session: Session) {
// Timber.d("createSessionSet...")
val realm = session.realm
val set = SessionSet.newInstance(realm)
set.startDate = session.startDate!!
set.endDate = session.endDate!!
set.breakDuration = session.breakDuration
session.sessionSet = set
set.computeStats()
// Timber.d("SET SESSION count = ${set.sessions?.size}")
//
// val t = 0
// val f = realm.where<SessionSet>().equalTo("sessions.type", t).findAll()
// Timber.d("CASH SET COUNT = ${f.size}")
}
/**
* Multiple session sets update:
* Merges all sets into one (delete all then create a new one)
*/
private fun mergeSessionGroups(session: Session, sessionSets: RealmResults<SessionSet>) {
// Timber.d("mergeSessionGroups")
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) {
// Timber.d("removeFromTimeline")
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)
// Timber.d(">>> sessionSet.deleteFromRealm")
sessionSet.deleteFromRealm()
sessions.forEach {
updateTimeline(it)
}
}
}
}

@ -1,17 +1,21 @@
package net.pokeranalytics.android.calculus package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import android.os.CountDownTimer
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.model.LiveOnline import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.CustomField import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.PerformanceKey
import net.pokeranalytics.android.ui.view.rows.StaticReport import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import net.pokeranalytics.android.util.extensions.writeAsync import timber.log.Timber
import kotlin.coroutines.CoroutineContext
interface NewPerformanceListener { interface NewPerformanceListener {
@ -20,8 +24,8 @@ interface NewPerformanceListener {
class ReportWhistleBlower(var context: Context) { class ReportWhistleBlower(var context: Context) {
// private var sessions: RealmResults<Session>? = null private var sessions: RealmResults<Session>? = null
// private var results: RealmResults<Result>? = null private var results: RealmResults<Result>? = null
private var currentTask: ReportTask? = null private var currentTask: ReportTask? = null
@ -29,25 +33,25 @@ class ReportWhistleBlower(var context: Context) {
private val listeners: MutableList<NewPerformanceListener> = mutableListOf() private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
private var paused: Boolean = false var paused: Boolean = false
// private var timer: CountDownTimer? = null private var timer: CountDownTimer? = null
init { init {
// val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
//
// this.sessions = realm.where(Session::class.java).findAll() this.sessions = realm.where(Session::class.java).findAll()
// this.sessions?.addChangeListener { _ -> this.sessions?.addChangeListener { _ ->
// requestReportLaunch() requestReportLaunch()
// } }
//
//// this.results = realm.where(Result::class.java).findAll() this.results = realm.where(Result::class.java).findAll()
//// this.results?.addChangeListener { _ -> this.results?.addChangeListener { _ ->
//// requestReportLaunch() requestReportLaunch()
//// } }
//
// realm.close() realm.close()
} }
fun addListener(newPerformanceListener: NewPerformanceListener) { fun addListener(newPerformanceListener: NewPerformanceListener) {
@ -62,23 +66,22 @@ class ReportWhistleBlower(var context: Context) {
// Timber.d(">>> Launch report") // Timber.d(">>> Launch report")
if (paused) { if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return return
} }
launchReportTask() this.timer?.cancel()
// this.timer?.cancel() val launchStart = 100L
// val timer = object: CountDownTimer(launchStart, launchStart) {
// val launchStart = 100L override fun onTick(p0: Long) { }
// val timer = object : CountDownTimer(launchStart, launchStart) {
// override fun onTick(p0: Long) {} override fun onFinish() {
// launchReportTask()
// override fun onFinish() { }
// launchReportTask() }
// } this.timer = timer
// } timer.start()
// this.timer = timer
// timer.start()
} }
@ -105,12 +108,8 @@ class ReportWhistleBlower(var context: Context) {
fun resume() { fun resume() {
this.paused = false this.paused = false
val realm = Realm.getDefaultInstance()
realm.writeAsync {
this.requestReportLaunch() this.requestReportLaunch()
} }
realm.close()
}
fun has(performanceId: String): Boolean { fun has(performanceId: String): Boolean {
return this.currentNotifications.contains(performanceId) return this.currentNotifications.contains(performanceId)
@ -134,10 +133,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private var cancelled = false private var cancelled = false
// private val coroutineContext: CoroutineContext var handler: (() -> Unit)? = null
// get() = Dispatchers.Default
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() { fun start() {
messages.add("Starting task...")
launchReports() launchReports()
} }
@ -145,8 +147,10 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
this.cancelled = true this.cancelled = true
} }
var messages: MutableList<String> = mutableListOf()
private fun launchReports() { private fun launchReports() {
// CoroutineScope(coroutineContext).launch { CoroutineScope(coroutineContext).launch {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -168,7 +172,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
launchReport(realm, StaticReport.CustomFieldList(customField)) launchReport(realm, StaticReport.CustomFieldList(customField))
} }
realm.close() realm.close()
// } }
} }
@ -193,14 +197,18 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
} }
private fun launchOptimalDuration(realm: Realm, report: StaticReport) { private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.values().forEach { key -> LiveOnline.entries.forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive) val duration = CashGameOptimalDurationCalculator.start(key.isLive)
analyseOptimalDuration(realm, report, key, duration) analyseOptimalDuration(realm, report, key, duration)
} }
this.handler?.let { it() }
} }
private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) { private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) {
messages.add("Analyse report $staticReport...")
val nameSeparator = " " val nameSeparator = " "
for (stat in result.options.stats) { for (stat in result.options.stats) {
@ -215,28 +223,24 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
customField?.let { customField?.let {
query = query.equalTo("customFieldId", it.id) query = query.equalTo("customFieldId", it.id)
} }
var currentPerf = query.findFirst() val currentPerf = query.findFirst()
if (currentPerf != null) {
currentPerf = realm.copyFromRealm(currentPerf)
}
// Store if necessary, delete if necessary // Store if necessary, delete if necessary
val bestComputedResults = result.max(stat) val bestComputedResults = result.max(stat)
bestComputedResults?.let { computedResults -> bestComputedResults?.let { computedResults ->
messages.add("found new perf...")
val performanceQuery = computedResults.group.query val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator) val performanceName = performanceQuery.getName(this.context, nameSeparator)
// Timber.d("Best computed = $performanceName, ${computedResults.computedStat(Stat.NET_RESULT)?.value}") Timber.d("Best computed = $performanceName, ${computedResults.computedStat(Stat.NET_RESULT)?.value}")
var storePerf = true var storePerf = true
currentPerf?.let { currentPerf?.let {
messages.add("has current perf...")
currentPerf.name?.let { name -> currentPerf.name?.let { name ->
if (computedResults.group.query.getName( if (computedResults.group.query.getName(this.context, nameSeparator) == name) {
this.context,
nameSeparator
) == name
) {
storePerf = false storePerf = false
} }
} }
@ -247,14 +251,16 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
} }
if (storePerf) { if (storePerf) {
realm.executeTransaction {
currentPerf.name = performanceName currentPerf.name = performanceName
currentPerf.objectId = performanceQuery.objectId currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id currentPerf.customFieldId = customField?.id
// realm.copyToRealm(currentPerf) }
this.whistleBlower.notify(currentPerf) this.whistleBlower.notify(currentPerf)
} }
} }
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) { if (currentPerf == null && storePerf) {
val performance = Performance( val performance = Performance(
@ -265,17 +271,20 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
customField?.id, customField?.id,
null null
) )
realm.copyToRealm(performance) realm.executeTransaction { it.copyToRealm(performance) }
this.whistleBlower.notify(performance) this.whistleBlower.notify(performance)
} }
} ?: run { // if there is no max but a now irrelevant Performance, we delete it } ?: run { // if there is no max but a now irrelevant Performance, we delete it
messages.add("deletes current perf if necessary: $currentPerf...")
// Timber.d("NO best computed value, current perf = $currentPerf ") // Timber.d("NO best computed value, current perf = $currentPerf ")
currentPerf?.let { perf -> currentPerf?.let { perf ->
realm.executeTransaction {
// Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}") // Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
realm.findById<Performance>(perf.id)?.deleteFromRealm() perf.deleteFromRealm()
// perf.deleteFromRealm() }
} }
} }
@ -283,17 +292,9 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
} }
private fun analyseOptimalDuration( private fun analyseOptimalDuration(realm: Realm, staticReport: StaticReport, key: PerformanceKey, duration: Double?) {
realm: Realm,
staticReport: StaticReport,
key: PerformanceKey,
duration: Double?
) {
var performance = performancesQuery(realm, staticReport, key).findFirst() val performance = performancesQuery(realm, staticReport, key).findFirst()
if (performance != null) {
performance = realm.copyFromRealm(performance)
}
duration?.let { duration?.let {
var storePerf = true var storePerf = true
@ -305,32 +306,30 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
} }
if (storePerf) { if (storePerf) {
realm.executeTransaction {
perf.name = formattedDuration perf.name = formattedDuration
perf.value = duration perf.value = duration
// realm.copyToRealm(perf) }
} }
} }
if (storePerf) { if (storePerf) {
val perf = val perf = Performance(staticReport, key, name = formattedDuration, value = duration)
Performance(staticReport, key, name = formattedDuration, value = duration) realm.executeTransaction { it.copyToRealm(perf) }
realm.copyToRealm(perf)
this.whistleBlower.notify(perf) this.whistleBlower.notify(perf)
} }
} ?: run { // no duration } ?: run { // no duration
performance?.let { perf -> performance?.let { perf ->
realm.findById<Performance>(perf.id)?.deleteFromRealm() realm.executeTransaction {
perf.deleteFromRealm() // delete if the perf exists
}
} }
} }
} }
private fun performancesQuery( private fun performancesQuery(realm: Realm, staticReport: StaticReport, key: PerformanceKey): RealmQuery<Performance> {
realm: Realm,
staticReport: StaticReport,
key: PerformanceKey
): RealmQuery<Performance> {
return realm.where(Performance::class.java) return realm.where(Performance::class.java)
.equalTo("reportId", staticReport.uniqueIdentifier) .equalTo("reportId", staticReport.uniqueIdentifier)
.equalTo("key", key.value) .equalTo("key", key.value)

@ -89,7 +89,7 @@ class BankrollCalculator {
this.computeRiskOfRuin(report, result) this.computeRiskOfRuin(report, result)
} else { } else {
val results = Filter.queryOn<Session>(realm, baseQuery) val results = Filter.queryOn<Result>(realm, baseQuery)
report.netResult = results.sum("net").toDouble() report.netResult = results.sum("net").toDouble()
} }

@ -2,8 +2,8 @@ package net.pokeranalytics.android.calculus.bankroll
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Bankroll
@ -68,12 +68,12 @@ object BankrollReportManager {
} }
// otherwise compute it // otherwise compute it
CoroutineScope(context = coroutineContext).launch { GlobalScope.launch(coroutineContext) {
var report: BankrollReport? = null var report: BankrollReport? = null
val coroutine = CoroutineScope(context = Dispatchers.IO).async { val coroutine = GlobalScope.async {
val s = Date() val s = Date()
// Timber.d(">>>>> start computing bankroll...") Timber.d(">>>>> start computing bankroll...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -84,7 +84,7 @@ object BankrollReportManager {
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
// Timber.d(">>>>> ended in $duration seconds") Timber.d(">>>>> ended in $duration seconds")
} }
coroutine.await() coroutine.await()

@ -8,6 +8,7 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import org.apache.commons.math3.fitting.PolynomialCurveFitter import org.apache.commons.math3.fitting.PolynomialCurveFitter
import org.apache.commons.math3.fitting.WeightedObservedPoints import org.apache.commons.math3.fitting.WeightedObservedPoints
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.round import kotlin.math.round

@ -87,7 +87,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
if (this is ValueCustomFields) { if (this is ValueCustomFields) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val distincts = realm.where<CustomFieldEntry>().equalTo("customField.id", this.customFieldId).findAll().sort("numericValue", Sort.ASCENDING) val distincts = realm.where<CustomFieldEntry>().equalTo("customFields.id", this.customFieldId).findAll().sort("numericValue", Sort.ASCENDING)
realm.close() realm.close()
val objects = mutableListOf<QueryCondition.CustomFieldNumberQuery>() val objects = mutableListOf<QueryCondition.CustomFieldNumberQuery>()

@ -51,17 +51,12 @@ enum class LiveData : Localizable {
} }
fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable { fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable {
val proxyItem: Deletable? = this.getData(realm, primaryKey)
val data = this.getData(realm, primaryKey) proxyItem?.let {
data?.let { return data } return realm.copyFromRealm(it)
} ?: run {
return this.newEntity() return this.newEntity()
}
// val proxyItem: Deletable? = this.getData(realm, primaryKey)
// proxyItem?.let {
// return realm.copyFromRealm(it)
// } ?: run {
// return this.newEntity()
// }
} }
private fun newEntity(): Deletable { private fun newEntity(): Deletable {
@ -73,7 +68,7 @@ enum class LiveData : Localizable {
primaryKey?.let { primaryKey?.let {
val t = realm.findById(this.relatedEntity, it) val t = realm.findById(this.relatedEntity, it)
t?.let { t?.let {
proxyItem = realm.copyFromRealm(t) // make an unmanaged object proxyItem = t
} }
} }
return proxyItem return proxyItem

@ -120,7 +120,6 @@ fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
.addTag(this.id) .addTag(this.id)
.build() .build()
// WorkManager.getInstance(context).enqueue(work)
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work) WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
} }

@ -3,10 +3,7 @@ package net.pokeranalytics.android.model.filter
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
/** /**
@ -66,7 +63,7 @@ class FilterHelper {
ComputableResult::class.java -> ComputableResult.fieldNameForQueryType(queryCondition) ComputableResult::class.java -> ComputableResult.fieldNameForQueryType(queryCondition)
SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition) SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition)
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition) Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
// Result::class.java -> Result.fieldNameForQueryType(queryCondition) Result::class.java -> Result.fieldNameForQueryType(queryCondition)
else -> { else -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}")) CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null null

@ -108,6 +108,7 @@ class Query {
return Query(this) return Query(this)
} }
/* /*
Returns the first object Id of any QueryCondition Returns the first object Id of any QueryCondition
*/ */

@ -96,9 +96,7 @@ sealed class QueryCondition : RowRepresentable {
inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? { inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let { FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
if (!realm.isInTransaction) {
realm.refresh() realm.refresh()
}
val distincts = when (T::class) { val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING) String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)

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

@ -4,15 +4,14 @@ import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.PokerAnalyticsApplication import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.util.BLIND_SEPARATOR import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.extensions.writeAsync
import java.text.NumberFormat import java.text.NumberFormat
class Patcher { class Patcher {
@ -62,7 +61,7 @@ class Patcher {
patchRatedAmounts() patchRatedAmounts()
} }
patchPerformances() patchPerformances(application)
} }
@ -71,8 +70,8 @@ class Patcher {
val transactionTypes = TransactionType.Value.values() val transactionTypes = TransactionType.Value.values()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
Seed.createDefaultTransactionTypes(transactionTypes, context, asyncRealm) Seed.createDefaultTransactionTypes(transactionTypes, context, realm)
} }
realm.close() realm.close()
@ -81,17 +80,19 @@ class Patcher {
private fun patchBreaks() { private fun patchBreaks() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
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.writeAsync { asyncRealm -> realm.executeTransaction {
val sets = asyncRealm.where(SessionSet::class.java).findAll()
val sessions = Filter.queryOn<Session>(asyncRealm, Query(QueryCondition.IsCash))
sets.forEach { sets.forEach {
it.computeStats() it.computeStats()
} }
sessions.forEach { sessions.forEach {
it.generateStakes() it.generateStakes()
it.defineHighestBet() it.defineHighestBet()
}
results.forEach {
it.computeNumberOfRebuy() it.computeNumberOfRebuy()
} }
} }
@ -102,8 +103,8 @@ class Patcher {
private fun patchDefaultTransactionTypes(context: Context) { private fun patchDefaultTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
val tts = asyncRealm.where(TransactionType::class.java).findAll() val tts = realm.where(TransactionType::class.java).findAll()
tts.forEach { tt -> tts.forEach { tt ->
tt.kind?.let { kind -> tt.kind?.let { kind ->
val value = TransactionType.Value.values()[kind] val value = TransactionType.Value.values()[kind]
@ -116,8 +117,8 @@ class Patcher {
private fun patchStakes() { private fun patchStakes() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
val sessions = asyncRealm.where(Session::class.java).findAll() val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session -> sessions.forEach { session ->
val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull() val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) } 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 -> handHistories.forEach { hh ->
val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull() val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) } val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
@ -142,8 +143,8 @@ class Patcher {
private fun patchNegativeLimits() { private fun patchNegativeLimits() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
val sessions = asyncRealm.where(Session::class.java).lessThan("limit", 0).findAll() val sessions = realm.where(Session::class.java).lessThan("limit", 0).findAll()
sessions.forEach { session -> sessions.forEach { session ->
session.limit = null session.limit = null
} }
@ -153,10 +154,10 @@ class Patcher {
private fun cleanBlindsFilters() { private fun cleanBlindsFilters() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
val blindFilterConditions = asyncRealm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll() val blindFilterConditions = realm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll()
val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id } val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id }
val filters = asyncRealm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll() val filters = realm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll()
filters.deleteAllFromRealm() filters.deleteAllFromRealm()
} }
realm.close() realm.close()
@ -169,12 +170,11 @@ class Patcher {
private fun patchSessionSet() { private fun patchSessionSet() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
asyncRealm.where(SessionSet::class.java).findAll().deleteAllFromRealm() realm.where(SessionSet::class.java).findAll().deleteAllFromRealm()
val sessions = asyncRealm.where(Session::class.java) val sessions = realm.where(Session::class.java).isNotNull("startDate").isNotNull("endDate").findAll()
.isNotNull("startDate").isNotNull("endDate").findAll()
sessions.forEach { session -> sessions.forEach { session ->
DataManager.updateTimeline(session) SessionSetManager.updateTimeline(session)
} }
} }
realm.close() realm.close()
@ -187,8 +187,8 @@ class Patcher {
*/ */
private fun patchComputableResults() { private fun patchComputableResults() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction {
val crs = asyncRealm.where(ComputableResult::class.java).findAll() val crs = realm.where(ComputableResult::class.java).findAll()
crs.forEach { cr -> crs.forEach { cr ->
cr.session?.let { cr.updateWith(it) } cr.session?.let { cr.updateWith(it) }
} }
@ -196,15 +196,13 @@ class Patcher {
realm.close() realm.close()
} }
private fun patchPerformances() { private fun patchPerformances(application: PokerAnalyticsApplication) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val sessionCount = realm.where<Session>().findAll().size val sessionCount = realm.where<Session>().findAll().size
val performanceCount = realm.where<Performance>().findAll().size val performanceCount = realm.where<Performance>().findAll().size
if (sessionCount > 1 && performanceCount == 0) { if (sessionCount > 1 && performanceCount == 0) {
realm.writeAsync { application.reportWhistleBlower?.requestReportLaunch()
DataManager.reportWhistleBlower?.requestReportLaunch()
}
} }
realm.close() realm.close()
} }
@ -212,8 +210,8 @@ class Patcher {
private fun patchZeroTable() { private fun patchZeroTable() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val zero = 0 val zero = 0
realm.writeAsync { asyncRealm -> val sessions = realm.where<Session>().equalTo("numberOfTables", zero).findAll()
val sessions = asyncRealm.where<Session>().equalTo("numberOfTables", zero).findAll() realm.executeTransaction {
sessions.forEach { s -> sessions.forEach { s ->
s.numberOfTables = 1 s.numberOfTables = 1
} }
@ -223,8 +221,8 @@ class Patcher {
private fun patchRatedAmounts() { private fun patchRatedAmounts() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> val transactions = realm.where<Transaction>().findAll()
val transactions = asyncRealm.where<Transaction>().findAll() realm.executeTransaction {
transactions.forEach { t -> transactions.forEach { t ->
t.computeRatedAmount() t.computeRatedAmount()
} }

@ -1,7 +1,6 @@
package net.pokeranalytics.android.model.migrations package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.DynamicRealmObject
import io.realm.RealmMigration import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber import timber.log.Timber
@ -336,47 +335,6 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++ currentVersion++
} }
// Migrate to version 15
if (currentVersion == 14) {
schema.get("Transaction")?.let { ts ->
schema.get("Session")?.let { ss ->
ss.addField("buyin", Double::class.java).setNullable("buyin", true)
.addField("cashout", Double::class.java).setNullable("cashout", true)
.addField("netResult", Double::class.java).setNullable("netResult", true)
.addField("net", Double::class.java)
.addField("tips", Double::class.java).setNullable("tips", true)
.addRealmListField("transactions", ts)
.addField("tournamentFinalPosition", Int::class.java).setNullable("tournamentFinalPosition", true)
.addField("numberOfRebuy", Double::class.java).setNullable("numberOfRebuy", true)
.transform { obj ->
val result = obj.get<DynamicRealmObject>("result")
obj.set("buyin", result.get("buyin"))
obj.set("cashout", result.get("cashout"))
obj.set("netResult", result.get("netResult"))
obj.set("net", result.get("net"))
obj.set("tips", result.get("tips"))
obj.set("tournamentFinalPosition", result.get("tournamentFinalPosition"))
obj.set("numberOfRebuy", result.get("numberOfRebuy"))
obj.set("transactions", result.get("transactions"))
}
}
}
schema.get("CustomFieldEntry")?.let { cfes ->
schema.get("CustomField")?.let { cfs ->
cfes.addRealmObjectField("customField", cfs)
cfs.transform { customField ->
val entries = customField.getList("entries")
for (entry in entries) {
entry.setObject("customField", customField)
}
}
}
}
currentVersion++
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -28,10 +28,12 @@ open class ComputableResult : RealmObject(), Filterable {
val rate = session.bankroll?.currency?.rate ?: 1.0 val rate = session.bankroll?.currency?.rate ?: 1.0
this.ratedNet = session.net * rate session.result?.let { result ->
this.isPositive = session.isPositive this.ratedNet = result.net * rate
this.ratedBuyin = (session.buyin ?: 0.0) * rate this.isPositive = result.isPositive
this.ratedTips = (session.tips ?: 0.0) * rate this.ratedBuyin = (result.buyin ?: 0.0) * rate
this.ratedTips = (result.tips ?: 0.0) * rate
}
this.bbNet = session.bbNet this.bbNet = session.bbNet
this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0 this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0

@ -3,13 +3,11 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import java.util.* import java.util.*
open class Currency : RealmObject(), Identifiable { open class Currency : RealmObject() {
companion object { companion object {
@ -19,10 +17,7 @@ open class Currency : RealmObject(), Identifiable {
} }
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
@Ignore
override val realmObjectClass: Class<out Identifiable> = Currency::class.java
/** /**
* The currency code of the currency, i.e. USD, EUR... * The currency code of the currency, i.e. USD, EUR...
@ -44,23 +39,18 @@ open class Currency : RealmObject(), Identifiable {
* The rate of the currency with the main currency * The rate of the currency with the main currency
*/ */
var rate: Double? = DEFAULT_RATE var rate: Double? = DEFAULT_RATE
set(value) {
field = value
DataManager.currencyToCompute(this)
}
fun refreshRelatedRatedValues() { fun refreshRelatedRatedValues() {
// Timber.d("refreshRelatedRatedValues of $code")
val rate = this.rate ?: DEFAULT_RATE val rate = this.rate ?: DEFAULT_RATE
val query = this.realm.where(ComputableResult::class.java) val query = this.realm.where(ComputableResult::class.java)
query.`in`("session.bankroll.currency.id", arrayOf(this.id)) query.`in`("session.bankroll.currency.id", arrayOf(this.id))
val cResults = query.findAll() val cResults = query.findAll()
cResults.forEach { computable -> cResults.forEach { computable ->
computable.session?.net?.let { computable.session?.result?.net?.let {
computable.ratedNet = it * rate computable.ratedNet = it * rate
} }
computable.session?.buyin?.let { computable.session?.result?.buyin?.let {
computable.ratedBuyin = it * rate computable.ratedBuyin = it * rate
} }

@ -24,8 +24,9 @@ import net.pokeranalytics.android.ui.view.rows.CustomFieldPropertiesRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.SimpleRow import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.extensions.writeAsync import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource { open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource {
@ -166,9 +167,10 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
return R.string.cf_entry_delete_popup_message return R.string.cf_entry_delete_popup_message
} }
override fun deleteDependencies(realm: Realm) { override fun deleteDependencies(realm: Realm) {
if (isValid) { if (isValid) {
val entries = realm.where<CustomFieldEntry>().equalTo("customField.id", id).findAll() val entries = realm.where<CustomFieldEntry>().equalTo("customFields.id", id).findAll()
entries.deleteAllFromRealm() entries.deleteAllFromRealm()
} }
} }
@ -228,7 +230,6 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
*/ */
fun addEntry(): CustomFieldEntry { fun addEntry(): CustomFieldEntry {
val entry = CustomFieldEntry() val entry = CustomFieldEntry()
entry.customField = this
this.entries.add(entry) this.entries.add(entry)
sortEntries() sortEntries()
updateRowRepresentation() updateRowRepresentation()
@ -249,11 +250,9 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
fun cleanupEntries() { // called when saving the custom field fun cleanupEntries() { // called when saving the custom field
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val ids = this.entriesToDelete.map { it.id } this.entriesToDelete.forEach { // entries are out of realm
realm.writeAsync { asyncRealm -> realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
ids.forEach { id -> // entries are out of realm
asyncRealm.where<CustomFieldEntry>().equalTo("id", id).findFirst()?.deleteFromRealm()
} }
} }
realm.close() realm.close()
@ -262,16 +261,37 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
fun getOrCreateEntry(realm: Realm, value: String): CustomFieldEntry { fun getOrCreateEntry(realm: Realm, value: String): CustomFieldEntry {
this.entries.find { it.value == value }?.let { this.entries.find { it.value == value }?.let {
Timber.d("L>> get")
return it return it
} ?: run { } ?: run {
Timber.d("L>> create")
val entry = realm.copyToRealm(CustomFieldEntry()) val entry = realm.copyToRealm(CustomFieldEntry())
entry.customField = this
entry.value = value entry.value = value
this.entries.add(entry) this.entries.add(entry)
return entry return entry
} }
} }
/**
* Clean the entries if the type is not a list & remove the deleted entries from realm
*/
// fun cleanEntries(realm: Realm) {
// realm.executeTransaction {
//
// if (!isListType) {
// entriesToDelete.addAll(entries)
// entries.clear()
// }
//
// // @TODO
// entriesToDelete.forEach {
// Timber.d("Delete entry: V=${it.value} N=${it.numericValue} / ID=${it.id}")
// realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
// }
// entriesToDelete.clear()
// }
// }
/** /**
* Returns a comparison criteria based on this custom field * Returns a comparison criteria based on this custom field
*/ */

@ -4,7 +4,9 @@ import android.content.Context
import android.text.InputType import android.text.InputType
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -41,14 +43,13 @@ open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, R
/** /**
* The inverse relationship with CustomField * The inverse relationship with CustomField
*/ */
// @LinkingObjects("entries") @LinkingObjects("entries")
// val customFields: RealmResults<CustomField>? = null val customFields: RealmResults<CustomField>? = null
var customField: CustomField? = null val customField: CustomField?
get() {
// get() { return this.customFields?.first()
// return this.customFields?.first() }
// }
/** /**
* The string value of the entry * The string value of the entry

@ -9,10 +9,7 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.*
@ -43,7 +40,6 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> { inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> {
val rootQuery = realm.where<T>() val rootQuery = realm.where<T>()
var realmQuery = query.queryWith(rootQuery) var realmQuery = query.queryWith(rootQuery)
// Timber.d("entity = ${realmQuery.typeQueried} > desc = ${realmQuery.description}")
sortField?.let { sortField?.let {
realmQuery = realmQuery.sort(it) realmQuery = realmQuery.sort(it)
} }
@ -60,11 +56,18 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
override val imageClickable: Boolean? override val imageClickable: Boolean?
get() = true get() = true
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
// the queryWith name // the queryWith name
var name: String = "" var name: String = ""
get() {
if (field.isEmpty()) {
return this.query.defaultName
}
return field
}
override var useCount: Int = 0 override var useCount: Int = 0
@ -106,7 +109,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
val previousCondition = filterConditions.filter { val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
} }
filterConditions.removeAll(previousCondition.toSet()) filterConditions.removeAll(previousCondition)
filterConditions.add(newFilterCondition) filterConditions.add(newFilterCondition)
} }
} }
@ -115,7 +118,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
fun remove(filterCategoryRow: FilterCategoryRow) { fun remove(filterCategoryRow: FilterCategoryRow) {
val sections = filterCategoryRow.filterSectionRows.map { it.name } val sections = filterCategoryRow.filterSectionRows.map { it.name }
val savedSections = filterConditions.filter { sections.contains(it.sectionName) } val savedSections = filterConditions.filter { sections.contains(it.sectionName) }
this.filterConditions.removeAll(savedSections.toSet()) this.filterConditions.removeAll(savedSections)
} }
fun countBy(filterCategoryRow: FilterCategoryRow): Int { fun countBy(filterCategoryRow: FilterCategoryRow): Int {

@ -2,11 +2,9 @@ package net.pokeranalytics.android.model.realm
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.LiveOnline import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.ui.view.rows.StaticReport import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.lookupForNameInAllTablesById import net.pokeranalytics.android.util.extensions.lookupForNameInAllTablesById
@ -18,13 +16,10 @@ interface PerformanceKey {
val value: Int val value: Int
} }
open class Performance() : RealmObject(), Identifiable { open class Performance() : RealmObject() {
@PrimaryKey @PrimaryKey
override var id: String = UUID.randomUUID().toString() var id: String = UUID.randomUUID().toString()
@Ignore
override val realmObjectClass: Class<out Identifiable> = Performance::class.java
constructor( constructor(
report: StaticReport, report: StaticReport,

@ -6,9 +6,9 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
@ -16,7 +16,6 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
import java.util.* import java.util.*
@ -68,7 +67,6 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
*/ */
fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options { fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options {
Timber.d("stats = ${this.statIds.size}")
val stats = this.statIds.map { Stat.valueByIdentifier(it) } val stats = this.statIds.map { Stat.valueByIdentifier(it) }
// Comparison criteria // Comparison criteria

@ -9,7 +9,6 @@ import io.realm.annotations.RealmClass
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
@RealmClass @RealmClass
open class Result : RealmObject(), Filterable { open class Result : RealmObject(), Filterable {
@ -23,7 +22,6 @@ open class Result : RealmObject(), Filterable {
} }
} }
/** /**
* The buyin amount * The buyin amount
*/ */
@ -31,7 +29,6 @@ open class Result : RealmObject(), Filterable {
set(value) { set(value) {
field = value field = value
this.computeNumberOfRebuy() this.computeNumberOfRebuy()
// SessionManager.sessionNetChanged(this.session)
this.computeNet(true) this.computeNet(true)
} }
@ -42,6 +39,9 @@ open class Result : RealmObject(), Filterable {
set(value) { set(value) {
field = value field = value
this.computeNet(true) this.computeNet(true)
if (value != null) {
this.session?.end()
}
} }
/** /**
@ -49,8 +49,20 @@ open class Result : RealmObject(), Filterable {
*/ */
var netResult: Double? = null var netResult: Double? = null
set(value) { set(value) {
// this.session?.bankroll?.let { bankroll ->
// if (bankroll.live) {
// throw PAIllegalStateException("Can't set net result on a live bankroll")
// }
// } ?: run {
// throw PAIllegalStateException("Session doesn't have any bankroll")
// }
field = value field = value
this.computeNet(false) this.computeNet(false)
if (value != null) {
this.session?.end()
}
} }
/** /**
@ -93,14 +105,14 @@ open class Result : RealmObject(), Filterable {
val isPositive: Int val isPositive: Int
get() { get() {
return if (session?.isTournament() == true) { return if (session?.isTournament() == true) {
if ((this.cashout ?: -1.0) >= 0.0) 1 else 0 // if cashout is null we want to count a negative session if ((this.cashout ?: -1.0) > 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else { } else {
if (this.net >= 0.0) 1 else 0 if (this.net >= 0.0) 1 else 0
} }
} }
// Computes the Net // Computes the Net
fun computeNet(withBuyin: Boolean? = null) { private fun computeNet(withBuyin: Boolean? = null) {
val transactionsSum = transactions.sumOf { it.amount } val transactionsSum = transactions.sumOf { it.amount }
@ -135,7 +147,7 @@ open class Result : RealmObject(), Filterable {
} }
// Computes the number of rebuy // Computes the number of rebuy
private fun computeNumberOfRebuy() { fun computeNumberOfRebuy() {
this.session?.let { this.session?.let {
if (it.isCashGame()) { if (it.isCashGame()) {
it.cgBiggestBet?.let { bb -> it.cgBiggestBet?.let { bb ->

@ -11,9 +11,9 @@ import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.Stakes import net.pokeranalytics.android.model.Stakes
@ -27,6 +27,7 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.filter.QueryCondition.* import net.pokeranalytics.android.model.filter.QueryCondition.*
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.*
@ -44,7 +45,7 @@ import java.util.Currency
typealias BB = Double typealias BB = Double
open class Session : RealmObject(), Savable, RowRepresentable, Timed, open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Timed,
TimeFilterable, Filterable, DatedBankrollGraphEntry, StakesHolder { TimeFilterable, Filterable, DatedBankrollGraphEntry, StakesHolder {
enum class Type(val value: String) { enum class Type(val value: String) {
@ -66,23 +67,24 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
companion object { 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() val session = Session()
session.result = Result()
val br: Bankroll? = bankroll ?: realm.where<Bankroll>().findFirst() if (bankroll != null) {
br?.let { session.bankroll = bankroll
session.bankroll = realm.copyFromRealm(br) } else {
session.bankroll = realm.where<Bankroll>().findFirst()
} }
session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal
session.limit = Limit.NO.ordinal session.limit = Limit.NO.ordinal
session.game = realm.where(Game::class.java).equalTo("shortName", "HE").findFirst()
val game = realm.where(Game::class.java).equalTo("shortName", "HE").findFirst() return if (managed) {
game?.let { realm.copyToRealm(session)
session.game = realm.copyFromRealm(game) } else {
session
} }
return session
} }
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? { fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
@ -100,9 +102,9 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
AnyStake::class.java -> "cgStakes" AnyStake::class.java -> "cgStakes"
NumberOfTable::class.java -> "numberOfTables" NumberOfTable::class.java -> "numberOfTables"
NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet" NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet"
NumberOfRebuy::class.java -> "numberOfRebuy" NumberOfRebuy::class.java -> "result.numberOfRebuy"
TournamentNumberOfPlayer::class.java -> "tournamentNumberOfPlayers" TournamentNumberOfPlayer::class.java -> "tournamentNumberOfPlayers"
TournamentFinalPosition::class.java -> "tournamentFinalPosition" TournamentFinalPosition::class.java -> "result.tournamentFinalPosition"
TournamentFee::class.java -> "tournamentEntryFee" TournamentFee::class.java -> "tournamentEntryFee"
StartedFromDate::class.java, StartedToDate::class.java, EndedFromDate::class.java, EndedToDate::class.java -> "startDate" StartedFromDate::class.java, StartedToDate::class.java, EndedFromDate::class.java, EndedToDate::class.java -> "startDate"
AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek" AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek"
@ -116,7 +118,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
Duration::class.java -> "netDuration" Duration::class.java -> "netDuration"
CustomFieldListQuery::class.java -> "customFieldEntries.id" CustomFieldListQuery::class.java -> "customFieldEntries.id"
CustomFieldAmountQuery::class.java, CustomFieldNumberQuery::class.java -> "customFieldEntries.numericValue" CustomFieldAmountQuery::class.java, CustomFieldNumberQuery::class.java -> "customFieldEntries.numericValue"
CustomFieldQuery::class.java -> "customFieldEntries.customField.id" CustomFieldQuery::class.java -> "customFieldEntries.customFields.id"
DateNotNull::class.java -> "startDate" DateNotNull::class.java -> "startDate"
EndDateNotNull::class.java -> "endDate" EndDateNotNull::class.java -> "endDate"
BiggestBetNotNull::class.java -> "cgBiggestBet" BiggestBetNotNull::class.java -> "cgBiggestBet"
@ -146,7 +148,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
val sessionType: Type val sessionType: Type
get() { return Type.values()[this.type] } get() { return Type.values()[this.type] }
// Not used anymore // The result of the main user
var result: Result? = null var result: Result? = null
@LinkingObjects("session") @LinkingObjects("session")
@ -207,7 +209,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
this.endDate = null this.endDate = null
} }
this.dateChanged() this.dateChanged()
this.addToComputeQueue() this.computeStats()
} }
/** /**
@ -228,7 +230,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
this.computeNetDuration() this.computeNetDuration()
this.dateChanged() this.dateChanged()
this.defineDefaultTournamentBuyinIfNecessary() this.defineDefaultTournamentBuyinIfNecessary()
this.addToComputeQueue() this.computeStats()
} }
/** /**
@ -238,7 +240,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
set(value) { set(value) {
field = value field = value
this.computeNetDuration() this.computeNetDuration()
this.addToComputeQueue() this.computeStats()
} }
/** /**
@ -250,6 +252,10 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
* The start date of the break * The start date of the break
*/ */
override var pauseDate: Date? = null override var pauseDate: Date? = null
set(value) {
field = value
// this.updateRowRepresentation()
}
// The session set containing the sessions, which can contain multiple endedSessions // The session set containing the sessions, which can contain multiple endedSessions
var sessionSet: SessionSet? = null var sessionSet: SessionSet? = null
@ -262,7 +268,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
set(value) { set(value) {
field = value field = value
this.generateStakes() this.generateStakes()
this.addToComputeQueue() this.computeStats()
// this.updateRowRepresentation() // this.updateRowRepresentation()
} }
@ -290,7 +296,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
set(value) { set(value) {
if (value > 0) { if (value > 0) {
field = value field = value
this.addToComputeQueue() this.computeStats()
} }
} }
@ -308,13 +314,16 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
// The small blind value // The small blind value
var cgOldSmallBlind: Double? = null var cgOldSmallBlind: Double? = null
set(value) {
field = value
}
// The big blind value // The big blind value
var cgOldBigBlind: Double? = null var cgOldBigBlind: Double? = null
set(value) { set(value) {
field = value field = value
this.addToComputeQueue() this.computeStats()
this.computeNumberOfRebuy() this.result?.computeNumberOfRebuy()
} }
// var blinds: String? = null // var blinds: String? = null
@ -325,8 +334,8 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
field = value field = value
this.generateStakes() this.generateStakes()
this.defineHighestBet() this.defineHighestBet()
this.addToComputeQueue() this.computeStats()
this.computeNumberOfRebuy() this.result?.computeNumberOfRebuy()
} }
var cgBlinds: String? = null var cgBlinds: String? = null
@ -334,8 +343,8 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
field = cleanupBlinds(value) field = cleanupBlinds(value)
this.generateStakes() this.generateStakes()
this.defineHighestBet() this.defineHighestBet()
this.addToComputeQueue() this.computeStats()
this.computeNumberOfRebuy() this.result?.computeNumberOfRebuy()
} }
var cgBiggestBet: Double? = null var cgBiggestBet: Double? = null
@ -348,7 +357,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
var tournamentEntryFee: Double? = null var tournamentEntryFee: Double? = null
set(value) { set(value) {
field = value field = value
this.computeNumberOfRebuy() this.result?.computeNumberOfRebuy()
} }
// The total number of players who participated in the tournament // The total number of players who participated in the tournament
@ -370,131 +379,9 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
var handsCount: Int? = null var handsCount: Int? = null
set(value) { set(value) {
field = value field = value
this.addToComputeQueue() this.computeStats()
}
/**
* The buyin amount
*/
var buyin: Double? = null
set(value) {
field = value
this.computeNumberOfRebuy()
this.computeNet(true)
}
/**
* The cashed out amount
*/
var cashout: Double? = null
set(value) {
field = value
this.computeNet(true)
}
/**
* The net result
*/
var netResult: Double? = null
set(value) {
field = value
this.computeNet(false)
}
/**
* The pre-computed net (readonly)
*/
var net: Double = 0.0
private set
/**
* Tips
*/
var tips: Double? = null
set(value) {
field = value
this.addToComputeQueue()
}
// The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList()
set(value) {
field = value
this.computeNet()
}
// The tournament final position, if applicable
var tournamentFinalPosition: Int? = null
// Number of rebuys
var numberOfRebuy: Double? = null
//////////////////////////////
// Computes the Net
fun computeNet(withBuyin: Boolean? = null) {
val transactionsSum = transactions.sumOf { it.amount }
// choose the method to compute the net
var useBuyin = withBuyin ?: true
if (withBuyin == null) {
if (netResult != null) {
useBuyin = false
} else if (buyin != null || cashout != null) {
useBuyin = true
} else {
if (this.isCashGame() && !this.isLive) {
useBuyin = false
}
}
} }
if (useBuyin) {
val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum
} else {
val netResult = this.netResult ?: 0.0
this.net = netResult + transactionsSum
}
// Precompute results
this.addToComputeQueue()
}
// Computes the number of rebuy
fun computeNumberOfRebuy() {
if (this.isCashGame()) {
this.cgBiggestBet?.let { bb ->
if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else {
this.numberOfRebuy = null
}
}
} else {
this.tournamentEntryFee?.let { entryFee ->
if (entryFee > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / entryFee
} else {
this.numberOfRebuy = null
}
}
}
}
val isPositive: Int
get() {
return if (this.isTournament()) {
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
}
}
//////////////////////////////
fun bankrollHasBeenUpdated() { fun bankrollHasBeenUpdated() {
this.generateStakes() this.generateStakes()
} }
@ -504,15 +391,11 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
* Should be called when the start / end date are changed * Should be called when the start / end date are changed
*/ */
private fun dateChanged() { private fun dateChanged() {
if (this.endDate != null) {
DataManager.sessionDateChanged(this) SessionSetManager.updateTimeline(this)
} else if (this.sessionSet != null) {
// if (this.endDate != null) { SessionSetManager.removeFromTimeline(this)
// SessionSetManager.updateTimeline(this) }
// } else if (this.sessionSet != null) {
// SessionSetManager.removeFromTimeline(this)
// }
// this.updateRowRepresentation() // this.updateRowRepresentation()
} }
@ -552,9 +435,9 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
val bbNet: BB val bbNet: BB
get() { get() {
val bb = this.cgBiggestBet val bb = this.cgBiggestBet
// val result = this.result val result = this.result
return if (bb != null) { return if (bb != null && result != null) {
this.net / bb result.net / bb
} else { } else {
0.0 0.0
} }
@ -585,14 +468,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
@Ignore @Ignore
override var amount: Double = 0.0 override var amount: Double = 0.0
get() { get() {
return this.net return this.result?.net ?: 0.0
}
/**
* Pre-compute various statIds
*/
private fun addToComputeQueue() {
DataManager.sessionToCompute(this)
} }
/** /**
@ -625,13 +501,17 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
val numberOfHandsPerHour: Double val numberOfHandsPerHour: Double
get() { get() {
val tableSize = this.tableSize ?: 9 // 9 is the default table size if null val tableSize = this.tableSize ?: 9 // 9 is the default table size if null
val dealtHandsPerHour = if (this.isLive) UserConfigObserver.liveDealtHandsPerHour else UserConfigObserver.onlineDealtHandsPerHour val config = UserConfig.getConfiguration(this.realm)
return this.numberOfTables * dealtHandsPerHour / tableSize.toDouble() val playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour
return this.numberOfTables * playerHandsPerHour / tableSize.toDouble()
} }
val hourlyRate: Double val hourlyRate: Double
get() { get() {
return this.net / this.hourlyDuration this.result?.let { result ->
return result.net / this.hourlyDuration
}
throw ModelException("Session should have an existing Result relationship")
} }
private val bbHourlyRate: BB private val bbHourlyRate: BB
@ -649,12 +529,12 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
val hasBuyin: Boolean val hasBuyin: Boolean
get() { get() {
return this.buyin != null return this.result?.buyin != null
} }
val hasNetResult: Boolean val hasNetResult: Boolean
get() { get() {
return this.netResult != null return this.result?.netResult != null
} }
// Manageable // Manageable
@ -677,6 +557,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
* Start or continue a session * Start or continue a session
*/ */
fun startOrContinue() { fun startOrContinue() {
realm.executeTransaction {
when (val state = getState()) { when (val state = getState()) {
SessionState.PENDING, SessionState.PLANNED -> { SessionState.PENDING, SessionState.PLANNED -> {
this.startDate = Date() this.startDate = Date()
@ -696,10 +577,11 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
} }
} }
} }
}
private fun defineDefaultTournamentBuyinIfNecessary() { private fun defineDefaultTournamentBuyinIfNecessary() {
if (this.tournamentEntryFee != null && this.buyin == null) { if (this.tournamentEntryFee != null && this.result?.buyin == null) {
this.buyin = this.tournamentEntryFee this.result?.buyin = this.tournamentEntryFee
} }
} }
@ -707,6 +589,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
* Pause a session * Pause a session
*/ */
fun pause() { fun pause() {
realm.executeTransaction {
when (val state = getState()) { when (val state = getState()) {
SessionState.STARTED -> { SessionState.STARTED -> {
this.pauseDate = Date() this.pauseDate = Date()
@ -714,17 +597,20 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
else -> throw PAIllegalStateException("Pausing a session in an unmanaged state: $state") else -> throw PAIllegalStateException("Pausing a session in an unmanaged state: $state")
} }
} }
}
/** /**
* Stop a session * Stop a session
*/ */
fun stop(context: Context) { fun stop(context: Context) {
realm.executeTransaction {
when (val state = getState()) { when (val state = getState()) {
SessionState.STARTED, SessionState.PAUSED -> { SessionState.STARTED, SessionState.PAUSED -> {
this.end() this.end()
} }
else -> throw Exception("Stopping session in unmanaged state: $state") else -> throw Exception("Stopping session in unmanaged state: $state")
} }
}
cancelStopNotification(context) cancelStopNotification(context)
} }
@ -732,11 +618,13 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
* Restart a session * Restart a session
*/ */
fun restart() { fun restart() {
realm.executeTransaction {
this.pauseDate = null this.pauseDate = null
this.startDate = Date() this.startDate = Date()
this.endDate = null this.endDate = null
this.breakDuration = 0L this.breakDuration = 0L
} }
}
/** /**
* Utility method to cleanly end a session * Utility method to cleanly end a session
@ -800,11 +688,10 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
if (isValid) { if (isValid) {
// CrashLogging.log("Deletes session. Id = ${this.id}") // CrashLogging.log("Deletes session. Id = ${this.id}")
realm.executeTransaction {
// realm.executeTransaction {
cleanup() cleanup()
deleteFromRealm() deleteFromRealm()
// } }
} else { } else {
CrashLogging.log("Attempt to delete an invalid session") CrashLogging.log("Attempt to delete an invalid session")
} }
@ -817,10 +704,10 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
// Updates the timeline // Updates the timeline
this.sessionSet?.let { this.sessionSet?.let {
DataManager.removeFromTimeline(this) SessionSetManager.removeFromTimeline(this)
} }
// cleanup unnecessary related objects // cleanup unnecessary related objects
// this.deleteFromRealm() this.result?.deleteFromRealm()
this.computableResults?.deleteAllFromRealm() this.computableResults?.deleteAllFromRealm()
} }
@ -851,7 +738,7 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
return "Session ${this.creationDate}" return "Session ${this.creationDate}"
} }
fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll? SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
@ -870,32 +757,23 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000 this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000
} }
SessionPropertiesRow.BUY_IN -> { SessionPropertiesRow.BUY_IN -> {
// val localResult = getOrCreateResult() val localResult = getOrCreateResult()
this.buyin = value as Double? localResult.buyin = value as Double?
} }
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> { SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
// val localResult = getOrCreateResult() val localResult = getOrCreateResult()
val cashOut = value as Double? localResult.cashout = value as Double?
this.cashout = cashOut
if (cashOut != null) {
this.end()
}
} }
SessionPropertiesRow.NET_RESULT -> { SessionPropertiesRow.NET_RESULT -> {
// val localResult = getOrCreateResult() val localResult = getOrCreateResult()
val netResult = value as Double? localResult.netResult = value as Double?
this.netResult = netResult
if (netResult != null) {
this.end()
}
} }
SessionPropertiesRow.COMMENT -> comment = value as String? ?: "" SessionPropertiesRow.COMMENT -> comment = value as String? ?: ""
SessionPropertiesRow.END_DATE -> if (value is Date?) { SessionPropertiesRow.END_DATE -> if (value is Date?) {
this.endDate = value this.endDate = value
} }
SessionPropertiesRow.GAME -> { SessionPropertiesRow.GAME -> {
when (value) { if (value is ArrayList<*>) {
is ArrayList<*> -> {
limit = try { limit = try {
(value[0] as Int?) (value[0] as Int?)
} catch (e: Exception) { } catch (e: Exception) {
@ -906,16 +784,13 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} } else if (value is Game) {
is Game -> {
game = value game = value
} } else if (value == null) {
null -> {
limit = null limit = null
game = null game = null
} }
} }
}
SessionPropertiesRow.INITIAL_BUY_IN -> { SessionPropertiesRow.INITIAL_BUY_IN -> {
this.tournamentEntryFee = (value as Double?) this.tournamentEntryFee = (value as Double?)
} }
@ -928,21 +803,21 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
} }
} }
SessionPropertiesRow.POSITION -> { SessionPropertiesRow.POSITION -> {
// val localResult = if (result != null) result as Result else realm.createObject(Result::class.java) val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)
if (value is Double) { if (value is Double) {
this.tournamentFinalPosition = value.toInt() localResult.tournamentFinalPosition = value.toInt()
} else { } else {
this.tournamentFinalPosition = null localResult.tournamentFinalPosition = null
} }
// result = localResult result = localResult
} }
SessionPropertiesRow.START_DATE -> if (value is Date) { SessionPropertiesRow.START_DATE -> if (value is Date) {
this.startDate = value this.startDate = value
} }
SessionPropertiesRow.TABLE_SIZE -> tableSize = value as Int? SessionPropertiesRow.TABLE_SIZE -> tableSize = value as Int?
SessionPropertiesRow.TIPS -> { SessionPropertiesRow.TIPS -> {
// val localResult = getOrCreateResult() val localResult = getOrCreateResult()
this.tips = value as Double? localResult.tips = value as Double?
} }
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName? SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName?
SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal
@ -967,7 +842,6 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
if (value != null) { if (value != null) {
val customFieldEntry = CustomFieldEntry() val customFieldEntry = CustomFieldEntry()
customFieldEntry.numericValue = value as Double? customFieldEntry.numericValue = value as Double?
customFieldEntry.customField = row
customFieldEntries.add(customFieldEntry) customFieldEntries.add(customFieldEntry)
row.entries.add(customFieldEntry) row.entries.add(customFieldEntry)
} }
@ -983,25 +857,14 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
} }
// private fun customFieldEntries(realm: Realm, customField: CustomField): List<CustomFieldEntry> { private fun getOrCreateResult(): Result {
// return this.result
//// val cfEntries = customField.entries ?: run {
//// val sessionEntries = this.customFieldEntries val result = realm.createObject(Result::class.java)
//// this.result = result
//// val entries = realm.where<CustomFieldEntry>() result
//// .`in`() }
// }
// return listOf()
// }
// private fun getOrCreateResult(): Result {
// return this.result
// ?: run {
// val result = realm.createObject(Result::class.java)
// this.result = result
// result
// }
// }
// Stat Entry // Stat Entry
@ -1016,13 +879,15 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
override fun formattedValue(stat: Stat): TextFormat { override fun formattedValue(stat: Stat): TextFormat {
this.result?.let { result ->
val value: Double? = when (stat) { val value: Double? = when (stat) {
Stat.NET_RESULT, Stat.AVERAGE, Stat.STANDARD_DEVIATION -> this.net Stat.NET_RESULT, Stat.AVERAGE, Stat.STANDARD_DEVIATION -> result.net
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> 1.0 Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> 1.0
Stat.AVERAGE_BUYIN -> this.buyin Stat.AVERAGE_BUYIN -> result.buyin
Stat.ROI -> { Stat.ROI -> {
this.buyin?.let { result.buyin?.let {
Stat.returnOnInvestment(this.net, it) Stat.returnOnInvestment(result.net, it)
} ?: run { } ?: run {
null null
} }
@ -1046,6 +911,10 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
return TextFormat(NULL_TEXT) return TextFormat(NULL_TEXT)
} }
} ?: run {
throw PAIllegalStateException("Asking for statIds on Session without Result")
}
} }
override fun legendValues( override fun legendValues(
@ -1096,25 +965,25 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = Session::class.java override val realmObjectClass: Class<out Identifiable> = Session::class.java
fun charSequenceForRow(row: RowRepresentable, context: Context, realm: Realm): String { fun charSequenceForRow(row: RowRepresentable, context: Context): String {
return when (row) { return when (row) {
SessionPropertiesRow.BANKROLL -> bankroll?.name ?: NULL_TEXT SessionPropertiesRow.BANKROLL -> bankroll?.name ?: NULL_TEXT
SessionPropertiesRow.STAKES -> getFormattedStakes() SessionPropertiesRow.STAKES -> getFormattedStakes()
SessionPropertiesRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT SessionPropertiesRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
SessionPropertiesRow.BUY_IN -> this.buyin?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.cashout?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.NET_RESULT -> this.netResult?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT SessionPropertiesRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.GAME -> getFormattedGame() SessionPropertiesRow.GAME -> getFormattedGame()
SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.LOCATION -> location?.name ?: NULL_TEXT SessionPropertiesRow.LOCATION -> location?.name ?: NULL_TEXT
SessionPropertiesRow.PLAYERS -> tournamentNumberOfPlayers?.toString() ?: NULL_TEXT SessionPropertiesRow.PLAYERS -> tournamentNumberOfPlayers?.toString() ?: NULL_TEXT
SessionPropertiesRow.POSITION -> this.tournamentFinalPosition?.toString() ?: NULL_TEXT SessionPropertiesRow.POSITION -> result?.tournamentFinalPosition?.toString() ?: NULL_TEXT
SessionPropertiesRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT SessionPropertiesRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT SessionPropertiesRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT
SessionPropertiesRow.TIPS -> this.tips?.toCurrency(currency) ?: NULL_TEXT SessionPropertiesRow.TIPS -> result?.tips?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.TOURNAMENT_TYPE -> { SessionPropertiesRow.TOURNAMENT_TYPE -> {
this.tournamentType?.let { this.tournamentType?.let {
TournamentType.values()[it].localizedTitle(context) TournamentType.values()[it].localizedTitle(context)
@ -1136,14 +1005,10 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
} }
} }
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT
SessionPropertiesRow.HANDS -> { SessionPropertiesRow.HANDS -> this.handHistories?.size.toString()
val handHistories = realm.where(HandHistory::class.java).equalTo("session.id", this.id).findAll()
handHistories.size.toString()
}
SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context) SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context)
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString() SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString()
is CustomField -> { is CustomField -> {
// Timber.d("entries count = ${customFieldEntries.size}")
customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry -> customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(currency) return customFieldEntry.getFormattedValue(currency)
} }
@ -1159,12 +1024,12 @@ open class Session : RealmObject(), Savable, RowRepresentable, Timed,
} }
fun clearBuyinCashedOut() { fun clearBuyinCashedOut() {
this.buyin = null this.result?.buyin = null
this.cashout = null this.result?.cashout = null
} }
fun clearNetResult() { fun clearNetResult() {
this.netResult = null this.result?.netResult = null
} }
private fun cleanupBlinds(blinds: String?): String? { private fun cleanupBlinds(blinds: String?): String? {

@ -19,7 +19,7 @@ import java.text.DateFormat
import java.util.* import java.util.*
open class SessionSet : RealmObject(), Timed, Filterable { open class SessionSet() : RealmObject(), Timed, Filterable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -61,7 +61,6 @@ open class SessionSet : RealmObject(), Timed, Filterable {
override var netDuration: Long = 0L override var netDuration: Long = 0L
fun computeStats() { fun computeStats() {
// Timber.d("compute > session count = ${this.sessions?.size}")
this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0 this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0 this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0 this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0
@ -76,7 +75,7 @@ open class SessionSet : RealmObject(), Timed, Filterable {
var ratedNet: Double = 0.0 var ratedNet: Double = 0.0
private val hourlyRate: Double val hourlyRate: Double
get() { get() {
return this.ratedNet / this.hourlyDuration return this.ratedNet / this.hourlyDuration
} }
@ -85,7 +84,7 @@ open class SessionSet : RealmObject(), Timed, Filterable {
var bbNet: BB = 0.0 var bbNet: BB = 0.0
private val bbHourlyRate: BB val bbHourlyRate: BB
get() { get() {
return this.bbNet / this.hourlyDuration return this.bbNet / this.hourlyDuration
} }
@ -102,12 +101,13 @@ open class SessionSet : RealmObject(), Timed, Filterable {
companion object { companion object {
fun newInstance(realm: Realm) : SessionSet { fun newInstance(realm: Realm) : SessionSet {
return realm.copyToRealm(SessionSet()) val sessionSet = SessionSet()
return realm.copyToRealm(sessionSet)
} }
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? { fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
Session.fieldNameForQueryType(queryCondition)?.let { fieldName -> Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$fieldName" return "sessions.$it"
} }
return null return null
} }

@ -2,18 +2,15 @@ package net.pokeranalytics.android.model.realm
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.util.UUID_SEPARATOR import net.pokeranalytics.android.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
import java.util.* import java.util.*
open class UserConfig : RealmObject() { open class UserConfig : RealmObject() {
companion object { companion object {
// these values are buffered to avoid the use of Realm instance
fun getConfiguration(realm: Realm): UserConfig { fun getConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config -> realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config return config
@ -32,12 +29,8 @@ open class UserConfig : RealmObject() {
var transactionTypeIds: String = "" var transactionTypeIds: String = ""
// fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) { fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) {
// this.transactionTypeIds = transactionTypes.joinToString(UUID_SEPARATOR) { it.id } this.transactionTypeIds = transactionTypes.joinToString(UUID_SEPARATOR) { it.id }
// }
fun setTransactionTypeIds(transactionTypeIds: Set<String>) {
this.transactionTypeIds = transactionTypeIds.joinToString(UUID_SEPARATOR)
} }
fun transactionTypes(realm: Realm): List<TransactionType> { fun transactionTypes(realm: Realm): List<TransactionType> {
@ -46,39 +39,3 @@ open class UserConfig : RealmObject() {
} }
} }
object UserConfigObserver {
private var userConfig: RealmResults<UserConfig>? = null
var liveDealtHandsPerHour: Int = 0
var onlineDealtHandsPerHour: Int = 0
init {
val realm = Realm.getDefaultInstance()
this.updateValues(realm)
this.userConfig = realm.where(UserConfig::class.java).findAll()
this.userConfig?.addChangeListener { results ->
this.updateValues(results.realm)
val userConfig = UserConfig.getConfiguration(results.realm)
this.liveDealtHandsPerHour = userConfig.liveDealtHandsPerHour
this.onlineDealtHandsPerHour = userConfig.onlineDealtHandsPerHour
}
realm.close()
}
fun create() { }
private fun updateValues(realm: Realm) {
val userConfig = UserConfig.getConfiguration(realm)
this.liveDealtHandsPerHour = userConfig.liveDealtHandsPerHour
this.onlineDealtHandsPerHour = userConfig.onlineDealtHandsPerHour
Timber.d("UserConfigObserver values updated: live = ${this.liveDealtHandsPerHour}, online = ${this.onlineDealtHandsPerHour}")
}
}

@ -190,24 +190,18 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/*** /***
* Configures a hand history with a [handSetup] * Configures a hand history with a [handSetup]
*/ */
fun configure(handSetup: HandSetup) { fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups) this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it } handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it } handSetup.ante?.let { this.ante = it }
handSetup.blinds?.let { this.blinds = it } handSetup.blinds?.let { this.blinds = it }
handSetup.session?.let { session -> this.session = handSetup.session
this.date = session.handHistoryAutomaticDate this.date = this.session?.handHistoryAutomaticDate ?: Date()
session.realm?.let { realm ->
this.session = realm.copyFromRealm(session)
} ?: run {
this.session = session
}
} ?: run {
this.date = Date()
}
this.createActions(handSetup) this.createActions(handSetup)
} }
@ -321,7 +315,13 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
* Creates and affect a PlayerSetup at the given [positionIndex] * Creates and affect a PlayerSetup at the given [positionIndex]
*/ */
fun createPlayerSetup(positionIndex: Int): PlayerSetup { fun createPlayerSetup(positionIndex: Int): PlayerSetup {
val playerSetup = PlayerSetup()
val playerSetup = if (this.realm != null) {
this.realm.createObject(PlayerSetup::class.java) }
else {
PlayerSetup()
}
playerSetup.position = positionIndex playerSetup.position = positionIndex
this.playerSetups.add(playerSetup) this.playerSetups.add(playerSetup)
return playerSetup return playerSetup
@ -479,7 +479,11 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean? val heroWins: Boolean?
get() { get() {
return this.heroIndex?.let { heroIndex -> return this.heroIndex?.let { heroIndex ->
heroIndex == this.largestWonPot?.position this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex } // this.winnerPots.any { it.position == heroIndex }
} ?: run { } ?: run {
null null
@ -642,6 +646,8 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pots.forEach { pot -> pots.forEach { pot ->
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList()) val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners // Distributes the pot for each winners
@ -657,6 +663,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
wp.amount += share wp.amount += share
} }
} }
}
} }
return wonPots.values return wonPots.values

@ -16,7 +16,7 @@ class DataUtils {
fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int { fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int {
var sessionQuery = realm.where(Session::class.java) var sessionQuery = realm.where(Session::class.java)
.equalTo("startDate", startDate) .equalTo("startDate", startDate)
.equalTo("net", net) .equalTo("result.net", net)
endDate?.let { endDate?.let {
sessionQuery = sessionQuery.equalTo("endDate", it) sessionQuery = sessionQuery.equalTo("endDate", it)

@ -11,11 +11,11 @@ import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
* Returns all significant parameters concatenated in a String * Returns all significant parameters concatenated in a String
* Not suitable for display * Not suitable for display
*/ */
private fun Session.parameterRepresentation(context: Context, realm: Realm): String { private fun Session.parameterRepresentation(context: Context): String {
var representation = "" var representation = ""
this.significantFields().forEach { this.significantFields().forEach {
representation += this.charSequenceForRow(it, context, realm) representation += this.charSequenceForRow(it, context)
} }
return representation return representation
@ -73,20 +73,15 @@ class FavoriteSessionFinder {
/** /**
* Copies the favorite session parameters on the [session] * Copies the favorite session parameters on the [session]
*/ */
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context, realm: Realm) { 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 -> favoriteSession?.let { fav ->
session.limit = fav.limit session.limit = fav.limit
fav.game?.let { game -> session.game = fav.game
session.game = realm.copyFromRealm(game) session.bankroll = fav.bankroll
}
fav.bankroll?.let { br ->
session.bankroll = realm.copyFromRealm(br)
}
session.tableSize = fav.tableSize session.tableSize = fav.tableSize
when (session.type) { when (session.type) {
@ -117,7 +112,7 @@ class FavoriteSessionFinder {
val counters = hashMapOf<String, Counter>() val counters = hashMapOf<String, Counter>()
lastSessions.forEach { session -> lastSessions.forEach { session ->
val representation = session.parameterRepresentation(context, realm) val representation = session.parameterRepresentation(context)
val counter = counters[representation] val counter = counters[representation]
if (counter != null) { if (counter != null) {
counter.increment() counter.increment()

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

@ -6,18 +6,21 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.NewPerformanceListener import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.databinding.ActivityHomeBinding import net.pokeranalytics.android.databinding.ActivityHomeBinding
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.BackupTask
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.findAll import net.pokeranalytics.android.util.extensions.findAll
import net.pokeranalytics.android.util.extensions.isSameMonth import net.pokeranalytics.android.util.extensions.isSameMonth
import java.util.* import java.util.*
@ -40,12 +43,13 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
} }
} }
private lateinit var currencies: RealmResults<Currency>
private var homePagerAdapter: HomePagerAdapter? = null private var homePagerAdapter: HomePagerAdapter? = null
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (binding.viewPager.currentItem == Tab.REPORTS.identifier) { if (binding.viewPager.currentItem == Tab.REPORTS.identifier) {
DataManager.reportWhistleBlower?.clearNotifications() this.paApplication.reportWhistleBlower?.clearNotifications()
} }
when (item.itemId) { when (item.itemId) {
@ -74,6 +78,7 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
AppGuard.requestPurchasesUpdate() AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed() this.homePagerAdapter?.activityResumed()
lookForCalendarBadge() lookForCalendarBadge()
checkForFailedBackups()
} }
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
@ -93,16 +98,32 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
} }
} }
observeRealmObjects()
initUI() initUI()
checkFirstLaunch() checkFirstLaunch()
DataManager.reportWhistleBlower?.addListener(this) this.paApplication.reportWhistleBlower?.addListener(this)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
DataManager.reportWhistleBlower?.removeListener(this) this.paApplication.reportWhistleBlower?.removeListener(this)
}
private fun observeRealmObjects() {
val realm = getRealm()
// observe currency changes
this.currencies = realm.where(Currency::class.java).findAll()
this.currencies.addChangeListener { currencies, _ ->
realm.executeTransaction {
currencies.forEach {
it.refreshRelatedRatedValues()
}
}
}
} }
/** /**
@ -145,10 +166,12 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
} }
override fun newBestPerformanceHandler() { override fun newBestPerformanceHandler() {
if (Preferences.showInAppBadges(this)) { if (Preferences.showInAppBadges(this)) {
binding.navigation.getOrCreateBadge(R.id.navigation_reports).isVisible = true binding.navigation.getOrCreateBadge(R.id.navigation_reports).isVisible = true
binding.navigation.getOrCreateBadge(R.id.navigation_reports).number = 1 binding.navigation.getOrCreateBadge(R.id.navigation_reports).number = 1
} }
} }
private fun lookForCalendarBadge() { private fun lookForCalendarBadge() {
@ -185,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()
}
}
}
} }

@ -9,13 +9,14 @@ import android.provider.MediaStore
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.util.ImageUtils import net.pokeranalytics.android.util.ImageUtils
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.*
open class MediaActivity : BaseActivity() { open class MediaActivity : BaseActivity() {
@ -51,11 +52,11 @@ open class MediaActivity : BaseActivity() {
val filesList = ArrayList<File>() val filesList = ArrayList<File>()
CoroutineScope(context = Dispatchers.IO).launch { GlobalScope.launch {
if (tempFile != null) { if (tempFile != null) {
tempFile?.let { tempFile?.let {
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
filesList.add(it) filesList.add(it)
getPictures(filesList) getPictures(filesList)
} }
@ -64,7 +65,7 @@ open class MediaActivity : BaseActivity() {
data.clipData?.let { clipData -> data.clipData?.let { clipData ->
try { try {
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
isLoadingNewPictures() isLoadingNewPictures()
} }
@ -77,7 +78,7 @@ open class MediaActivity : BaseActivity() {
filesList.add(photoFile) filesList.add(photoFile)
} }
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
getPictures(filesList) getPictures(filesList)
} }
@ -89,7 +90,7 @@ open class MediaActivity : BaseActivity() {
data.data?.let { uri -> data.data?.let { uri ->
try { try {
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
isLoadingNewPictures() isLoadingNewPictures()
} }
@ -97,7 +98,7 @@ open class MediaActivity : BaseActivity() {
val photoFile = ImageUtils.createTempImageFile(this@MediaActivity) val photoFile = ImageUtils.createTempImageFile(this@MediaActivity)
ImageUtils.copyInputStreamToFile(inputStream!!, photoFile) ImageUtils.copyInputStreamToFile(inputStream!!, photoFile)
filesList.add(photoFile) filesList.add(photoFile)
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
getPictures(filesList) getPictures(filesList)
} }
@ -175,6 +176,13 @@ open class MediaActivity : BaseActivity() {
} }
} }
// // Test if we have the permission
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// selectedChoice = SELECTED_CHOICE_TAKE_PICTURE
// askForStoragePermission()
// return
// }
} }
@ -198,6 +206,21 @@ open class MediaActivity : BaseActivity() {
} }
} }
// Test if we have the permission
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// selectedChoice = SELECTED_CHOICE_SELECT_PICTURE
// askForStoragePermission()
// return
// }
//
// this.multiplePictures = multiplePictures
//
// val galleryIntent = Intent()
// galleryIntent.type = "image/*"
// galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures)
// galleryIntent.action = Intent.ACTION_GET_CONTENT
// startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
} }
/** /**

@ -4,6 +4,8 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.charts.LineChart
@ -101,6 +103,7 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
initData() initData()
initUI() initUI()
loadGraph() loadGraph()
} }
private fun initData() { private fun initData() {

@ -8,11 +8,10 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.databinding.FragmentImportBinding import net.pokeranalytics.android.databinding.FragmentImportBinding
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter import net.pokeranalytics.android.util.csv.CSVImporter
@ -64,6 +63,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
this.initUI() this.initUI()
this.startImport() this.startImport()
} }
@ -91,14 +91,14 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private fun startImport() { private fun startImport() {
DataManager.reportWhistleBlower?.pause() this.parentActivity?.paApplication?.reportWhistleBlower?.pause()
this.importer = CSVImporter(uri, requireContext()) this.importer = CSVImporter(uri, requireContext())
this.importer.delegate = this this.importer.delegate = this
CoroutineScope(coroutineContext).launch { CoroutineScope(coroutineContext).launch {
val coroutine = CoroutineScope(context = Dispatchers.IO).async { val coroutine = GlobalScope.async {
val s = Date() val s = Date()
Timber.d(">>> Start Import...") Timber.d(">>> Start Import...")
@ -142,7 +142,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
} }
private fun end() { private fun end() {
DataManager.reportWhistleBlower?.resume() this.parentActivity?.paApplication?.reportWhistleBlower?.resume()
activity?.finish() activity?.finish()
} }

@ -1,6 +1,8 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -8,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
@ -18,6 +21,8 @@ import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -26,7 +31,7 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.calculus.DataManager import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -42,7 +47,7 @@ import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.Date
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) { data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
@ -187,20 +192,34 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
ReportCreationActivity.newInstanceForResult(this, requireContext()) ReportCreationActivity.newInstanceForResult(this, requireContext())
} }
DataManager.reportWhistleBlower?.addListener(this) val sessionCount = getRealm().where(Result::class.java).count()
binding.computeButton.isVisible = adapterRows.isEmpty() && sessionCount > 5
binding.computeButton.setOnClickListener {
try {
forceReportWhistleBlowerStart()
} catch (e: Exception) {
e.message?.let {
this.showSnackBar(it)
}
}
}
this.paApplication?.reportWhistleBlower?.addListener(this)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
DataManager.reportWhistleBlower?.removeListener(this) this.paApplication?.reportWhistleBlower?.removeListener(this)
} }
// Rows // Rows
private fun updateRows() { private fun updateRows() {
this.adapterRows.clear() this.adapterRows.clear()
if (this.reportSetups.size > 0) { if (this.reportSetups.isNotEmpty()) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom)) adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups) adapterRows.addAll(this.reportSetups)
} }
@ -266,7 +285,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
override fun boolForRow(row: RowRepresentable): Boolean { override fun boolForRow(row: RowRepresentable): Boolean {
val reportRow = row as PerformanceRow val reportRow = row as PerformanceRow
return Preferences.showInAppBadges(requireContext()) return Preferences.showInAppBadges(requireContext())
&& (DataManager.reportWhistleBlower?.has(reportRow.performance.id) ?: false) && (this.paApplication?.reportWhistleBlower?.has(reportRow.performance.id) ?: false)
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
@ -322,7 +341,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
val report = Calculator.computeStats(realm, options = options) val report = Calculator.computeStats(realm, options = options)
// Timber.d("launchComputation: ${System.currentTimeMillis() - startDate.time}ms") Timber.d("launchComputation: ${System.currentTimeMillis() - startDate.time}ms")
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (!isDetached) { if (!isDetached) {
@ -335,7 +354,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
} }
override fun newBestPerformanceHandler() { override fun newBestPerformanceHandler() {
// Timber.d("newBestPerformanceHandler called") Timber.d("newBestPerformanceHandler called")
activity?.runOnUiThread { activity?.runOnUiThread {
this.dataListAdapter.notifyDataSetChanged() this.dataListAdapter.notifyDataSetChanged()
@ -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)
}
} }

@ -16,6 +16,7 @@ import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
@ -44,17 +45,21 @@ import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.* 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.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.csv.ProductCSVDescriptors import net.pokeranalytics.android.util.csv.ProductCSVDescriptors
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import net.pokeranalytics.android.util.extensions.writeAsync
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.Date
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener { class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -188,17 +193,17 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
private fun updateMainCurrency(currencyCode: String, rate: Double) { private fun updateMainCurrency(currencyCode: String, rate: Double) {
Preferences.setCurrencyCode(currencyCode, requireContext()) Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance()
getRealm().writeAsync { asyncRealm -> realm.executeTransaction {
asyncRealm.where(Currency::class.java).findAll().forEach { currency -> realm.where(Currency::class.java).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate currency.rate = (currency.rate ?: 1.0) * rate
} }
asyncRealm.where(Session::class.java).findAll().forEach { session -> realm.where(Session::class.java).findAll().forEach { session ->
session.bankrollHasBeenUpdated() session.bankrollHasBeenUpdated()
} }
} }
realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY) settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
} }
@ -319,6 +324,8 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed // completed
} }
} else { } else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result. // There was some problem, continue regardless of the result.
} }
} }

@ -9,7 +9,7 @@ import android.view.*
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -29,6 +29,7 @@ import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode
import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.modules.filter.FiltersActivity import net.pokeranalytics.android.ui.modules.filter.FiltersActivity
import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity
import timber.log.Timber
import java.util.* import java.util.*
class StatisticsFragment : FilterableFragment(), RealmAsyncListener { class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
@ -74,7 +75,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
addRealmChangeListener(this, UserConfig::class.java) addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java) addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, SessionSet::class.java)
addRealmChangeListener(this, Transaction::class.java) addRealmChangeListener(this, Transaction::class.java)
} }
@ -160,7 +160,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
CoroutineScope(coroutineContext).launch { CoroutineScope(coroutineContext).launch {
val async = CoroutineScope(context = Dispatchers.IO).async { val async = GlobalScope.async {
val s = Date() val s = Date()
// Timber.d(">>> start...") // Timber.d(">>> start...")
@ -222,7 +222,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
Stat.AVERAGE_BUYIN Stat.AVERAGE_BUYIN
) )
if (QueryCondition.IsCash.queryWith(realm.where(Session::class.java).isNotNull("tips")).count() > 0) { if (QueryCondition.IsCash.queryWith(realm.where(Result::class.java).isNotNull("tips")).count() > 0) {
cgStats.add(Stat.TOTAL_TIPS) cgStats.add(Stat.TOTAL_TIPS)
} }
@ -235,7 +235,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_GAMES,
Stat.AVERAGE_BUYIN Stat.AVERAGE_BUYIN
) )
if (QueryCondition.IsTournament.queryWith(realm.where(Session::class.java).isNotNull("tips")).count() > 0) { if (QueryCondition.IsTournament.queryWith(realm.where(Result::class.java).isNotNull("tips")).count() > 0) {
tStats.add(Stat.TOTAL_TIPS) tStats.add(Stat.TOTAL_TIPS)
} }

@ -114,7 +114,7 @@ class Top10Fragment : RealmFragment(), RowRepresentableDataSource, RowRepresenta
this.positiveSessions = getRealm().where<Session>() this.positiveSessions = getRealm().where<Session>()
.isNotNull("startDate") .isNotNull("startDate")
.isNotNull("endDate") .isNotNull("endDate")
.greaterThan("net", 0.0) .greaterThan("result.net", 0.0)
.findAll() .findAll()
updateTop10() updateTop10()

@ -3,6 +3,8 @@ package net.pokeranalytics.android.ui.fragment.components
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import net.pokeranalytics.android.PokerAnalyticsApplication import net.pokeranalytics.android.PokerAnalyticsApplication
@ -39,6 +41,12 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
v.setPadding(0, statusBarHeight, 0, 0)
insets
}
} }
override fun onResume() { override fun onResume() {

@ -8,16 +8,15 @@ import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.realm.RealmObject import io.realm.RealmObject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.util.extensions.writeAsync import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
/** /**
* Deletable Item Fragment * Deletable Item Fragment
@ -57,7 +56,7 @@ abstract class DeletableItemFragment : RealmFragment() {
val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName)
itemToDeleteId?.let { id -> itemToDeleteId?.let { id ->
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
delay(300) delay(300)
deleteItem(dataListAdapter, deletableItems(), id) deleteItem(dataListAdapter, deletableItems(), id)
} }
@ -89,8 +88,8 @@ abstract class DeletableItemFragment : RealmFragment() {
if (itemToDelete.isValidForDelete(this.getRealm())) { if (itemToDelete.isValidForDelete(this.getRealm())) {
deletedItem = getRealm().copyFromRealm(itemToDelete) deletedItem = getRealm().copyFromRealm(itemToDelete)
lastDeletedItemPosition = itemPosition lastDeletedItemPosition = itemPosition
getRealm().writeAsync { asyncRealm -> getRealm().executeTransaction {
itemToDelete.deleteDependencies(asyncRealm) itemToDelete.deleteDependencies(it)
itemToDelete.deleteFromRealm() itemToDelete.deleteFromRealm()
} }
itemHasBeenReInserted = false itemHasBeenReInserted = false
@ -118,9 +117,9 @@ abstract class DeletableItemFragment : RealmFragment() {
snackBar?.setAction(R.string.cancel) { snackBar?.setAction(R.string.cancel) {
if (!itemHasBeenReInserted) { if (!itemHasBeenReInserted) {
itemHasBeenReInserted = true itemHasBeenReInserted = true
getRealm().writeAsync { asyncRealm -> getRealm().executeTransaction { realm ->
deletedItem?.let { deletedItem?.let {
val item = asyncRealm.copyToRealmOrUpdate(it) val item = realm.copyToRealmOrUpdate(it)
updateUIAfterUndoDeletion(item) updateUIAfterUndoDeletion(item)
} }
} }

@ -4,13 +4,14 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -57,11 +58,12 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
parentActivity?.registerReceiver(
updateFilterUIBroadcast, IntentFilter( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
INTENT_FILTER_UPDATE_FILTER_UI 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() { override fun onDestroy() {
@ -140,7 +142,7 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
viewGroup.removeAllViews() viewGroup.removeAllViews()
viewGroup.addView(layoutCurrentFilter) viewGroup.addView(layoutCurrentFilter)
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
delay(300) delay(300)
viewGroup.visibility = View.VISIBLE viewGroup.visibility = View.VISIBLE
} }
@ -153,7 +155,7 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
*/ */
private fun hideSelectedFilter() { private fun hideSelectedFilter() {
view?.findViewById<ViewGroup>(R.id.selectedFilter).let { view?.findViewById<ViewGroup>(R.id.selectedFilter).let {
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
it?.visibility = View.GONE it?.visibility = View.GONE
} }
} }

@ -4,11 +4,14 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
interface RealmAsyncListener { interface RealmAsyncListener {
@ -69,7 +72,7 @@ open class RealmFragment : BaseFragment() {
this.changeListener = listener this.changeListener = listener
val results = this.realm.where(clazz).findAllAsync() val results = this.realm.where(clazz).findAllAsync()
results.addChangeListener { t, _ -> results.addChangeListener { t, _ ->
// Timber.d("Realm changes: ${realmResults?.size}, $this") Timber.d("Realm changes: ${realmResults?.size}, $this")
this.changeListener?.asyncListenedEntityChange(t.realm) this.changeListener?.asyncListenedEntityChange(t.realm)
} }
this.observedRealmResults.add(results) this.observedRealmResults.add(results)

@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.core.view.get import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import io.realm.RealmObject
import net.pokeranalytics.android.databinding.BottomSheetGameListBinding import net.pokeranalytics.android.databinding.BottomSheetGameListBinding
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
@ -39,11 +38,10 @@ class BottomSheetListGameFragment : BottomSheetListFragment() {
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
this.model.realmData?.let { results -> this.model.realmData?.let {
val selectedData = results[position] val selectedData = it[position]
(selectedData as? RealmObject)?.let { data -> selectedData?.let { data ->
val obj = results.realm.copyFromRealm(data) this.model.someValues[1] = data
this.model.someValues[1] = obj
this.onRowValueChanged() this.onRowValueChanged()
// this.delegate.onRowValueChanged(values, this.row) // this.delegate.onRowValueChanged(values, this.row)
dismiss() dismiss()

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

@ -17,7 +17,6 @@ import net.pokeranalytics.android.ui.modules.data.DataManagerFragment
import net.pokeranalytics.android.ui.viewmodel.ReportViewModel import net.pokeranalytics.android.ui.viewmodel.ReportViewModel
import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
abstract class AbstractReportFragment : DataManagerFragment() { abstract class AbstractReportFragment : DataManagerFragment() {
@ -85,7 +84,7 @@ abstract class AbstractReportFragment : DataManagerFragment() {
saveReport(nameEditText.text.toString()) saveReport(nameEditText.text.toString())
dialog.dismiss() dialog.dismiss()
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show()
} }
} }
.setNegativeButton(R.string.cancel) { dialog, _ -> .setNegativeButton(R.string.cancel) { dialog, _ ->
@ -106,18 +105,17 @@ abstract class AbstractReportFragment : DataManagerFragment() {
} }
this.reportViewModel.title = name this.reportViewModel.title = name
getRealm().writeAsync { asyncRealm ->
val rs = this.model.item as ReportSetup val rs = this.model.item as ReportSetup
getRealm().executeTransaction { realm ->
val firstSave = (this.model.primaryKey == null)
if (firstSave) {
val options = this.selectedReport.options val options = this.selectedReport.options
rs.name = name rs.name = name
rs.display = this.reportViewModel.reportDisplay?.ordinal rs.display = this.reportViewModel.reportDisplay?.ordinal ?: throw PAIllegalStateException("Display not set")
?: throw PAIllegalStateException("Display not set") options.stats.forEach {
rs.statIds.clear() rs.statIds.add(it.uniqueIdentifier)
rs.statIds.addAll(options.stats.map { it.uniqueIdentifier }) }
rs.criteriaIds.clear()
options.criterias.forEach { criteria -> options.criterias.forEach { criteria ->
when (criteria) { when (criteria) {
is CustomFieldCriteria -> rs.criteriaCustomFieldIds.add(criteria.customFieldId) is CustomFieldCriteria -> rs.criteriaCustomFieldIds.add(criteria.customFieldId)
@ -126,14 +124,17 @@ abstract class AbstractReportFragment : DataManagerFragment() {
} }
options.filterId?.let { id -> options.filterId?.let { id ->
rs.filter = asyncRealm.findById(id) rs.filter = realm.findById(id)
}
realm.copyToRealmOrUpdate(rs)
} else {
rs.name = name
realm.insertOrUpdate(rs)
} }
asyncRealm.copyToRealmOrUpdate(rs)
this.model.primaryKey = rs.id
} }
this.model.primaryKey = rs.id
this.deleteButtonShouldAppear = true this.deleteButtonShouldAppear = true
setToolbarTitle(this.reportViewModel.title) setToolbarTitle(this.reportViewModel.title)
} }

@ -5,10 +5,12 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -30,6 +32,7 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.StatRow import net.pokeranalytics.android.ui.view.rows.StatRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import timber.log.Timber
import java.util.* import java.util.*
open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, CoroutineScope, open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, CoroutineScope,
@ -81,6 +84,11 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
initData() initData()
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
report?.let { report?.let {
showResults() showResults()
} }
@ -202,10 +210,10 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
showLoader() showLoader()
CoroutineScope(context = Dispatchers.IO).launch(coroutineContext) { GlobalScope.launch(coroutineContext) {
var report: Report? = null var report: Report? = null
val test = CoroutineScope(context = Dispatchers.IO).async { val test = GlobalScope.async {
val s = Date() val s = Date()
// Timber.d(">>> start...") // Timber.d(">>> start...")
@ -220,7 +228,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> ended in $duration seconds") Timber.d(">>> ended in $duration seconds")
} }
test.await() test.await()

@ -11,8 +11,8 @@ import com.github.mikephil.charting.data.LineDataSet
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.AggregationType import net.pokeranalytics.android.calculus.AggregationType
@ -30,6 +30,7 @@ import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.helpers.AppReviewManager import net.pokeranalytics.android.ui.helpers.AppReviewManager
import timber.log.Timber
import java.util.* import java.util.*
@ -162,7 +163,7 @@ class ProgressReportFragment : AbstractReportFragment() {
graphContainer.hideWithAnimation() graphContainer.hideWithAnimation()
progressBar.showWithAnimation() progressBar.showWithAnimation()
CoroutineScope(context = Dispatchers.IO).launch { GlobalScope.launch {
val s = Date() val s = Date()
// Timber.d(">>> start...") // Timber.d(">>> start...")
@ -177,7 +178,7 @@ class ProgressReportFragment : AbstractReportFragment() {
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> ended in $duration seconds") Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
setGraphData(report, aggregationType) setGraphData(report, aggregationType)

@ -9,8 +9,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.data.LineDataSet
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -35,6 +35,8 @@ import net.pokeranalytics.android.ui.view.rows.BankrollTotalRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.sorted import net.pokeranalytics.android.util.extensions.sorted
import timber.log.Timber import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
interface BankrollRowRepresentable : RowRepresentable { interface BankrollRowRepresentable : RowRepresentable {
var bankrollId: String? var bankrollId: String?
@ -86,7 +88,7 @@ class BankrollFragment : DeletableItemFragment(), StaticRowRepresentableDataSour
if (requestCode == RequestCode.BANKROLL_DETAILS.value && resultCode == Activity.RESULT_OK) { if (requestCode == RequestCode.BANKROLL_DETAILS.value && resultCode == Activity.RESULT_OK) {
val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName) val itemToDeleteId = data?.getStringExtra(DataListActivity.IntentKey.ITEM_DELETED.keyName)
itemToDeleteId?.let { id -> itemToDeleteId?.let { id ->
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
delay(300) delay(300)
deleteItem(dataListAdapter, bankrolls, id) deleteItem(dataListAdapter, bankrolls, id)

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

@ -11,6 +11,7 @@ import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
@ -38,6 +39,7 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.set import kotlin.collections.set
@ -346,16 +348,16 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
binding.progressBar.showWithAnimation() binding.progressBar.showWithAnimation()
binding.recyclerView.hideWithAnimation() binding.recyclerView.hideWithAnimation()
CoroutineScope(context = Dispatchers.IO).launch { GlobalScope.launch {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
// realm.refresh() realm.refresh()
launchStatComputation(realm) launchStatComputation(realm)
realm.close() realm.close()
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
displayData() displayData()
} }
} }
@ -488,7 +490,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
sortedMonthlyReports = monthlyReports.toSortedMap(compareByDescending { it }) sortedMonthlyReports = monthlyReports.toSortedMap(compareByDescending { it })
sortedYearlyReports = yearlyReports.toSortedMap(compareByDescending { it }) sortedYearlyReports = yearlyReports.toSortedMap(compareByDescending { it })
// Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms") Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms")
} }
@ -496,7 +498,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
* Display data * Display data
*/ */
private fun displayData() { private fun displayData() {
// Timber.d("displayData") Timber.d("displayData")
if (context == null) { return } // required because of launchAsyncStatComputation if (context == null) { return } // required because of launchAsyncStatComputation
@ -592,8 +594,8 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
} }
} }
// Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms") Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
// Timber.d("Rows: ${rows.size}") Timber.d("Rows: ${rows.size}")
this.calendarAdapter.notifyDataSetChanged() this.calendarAdapter.notifyDataSetChanged()

@ -12,6 +12,7 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import java.util.* import java.util.*
import kotlin.collections.HashMap
class GridCalendarViewModel : ViewModel(), RowRepresentableDataSource { class GridCalendarViewModel : ViewModel(), RowRepresentableDataSource {
@ -63,8 +64,8 @@ class GridCalendarViewModel : ViewModel(), RowRepresentableDataSource {
while (tmpDate.time <= lastDate.time) { while (tmpDate.time <= lastDate.time) {
val result = groupedSessions[tmpDate]?.let { bucket -> val result = groupedSessions[tmpDate]?.let { bucket ->
if (bucket.sumOf { if (bucket.sumByDouble {
it?.net ?: 0.0 it.result?.net ?: 0.0
} > 0.0) CellResult.POSITIVE else CellResult.NEGATIVE } > 0.0) CellResult.POSITIVE else CellResult.NEGATIVE
} ?: run { } ?: run {
CellResult.EMPTY CellResult.EMPTY

@ -16,8 +16,6 @@ import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel
import net.pokeranalytics.android.util.extensions.writeAsync
import timber.log.Timber
open class DataManagerFragment : RealmFragment() { open class DataManagerFragment : RealmFragment() {
@ -98,26 +96,13 @@ open class DataManagerFragment : RealmFragment() {
val status = savable.getSaveValidityStatus(realm = this.getRealm()) val status = savable.getSaveValidityStatus(realm = this.getRealm())
when (status) { when (status) {
SaveValidityStatus.VALID -> { SaveValidityStatus.VALID -> {
this.getRealm().executeTransaction {
val realm = getRealm() val managedItem = it.copyToRealmOrUpdate(this.model.item)
val item = this.model.item if (managedItem is Savable) {
Timber.d("save") val uniqueIdentifier = managedItem.id
realm.writeAsync { asyncRealm ->
Timber.d("execute async")
asyncRealm.copyToRealmOrUpdate(item)
}
if (item is Savable) {
val uniqueIdentifier = item.id
finishActivityWithResult(uniqueIdentifier) finishActivityWithResult(uniqueIdentifier)
} }
}
// this.getRealm().executeTransaction {
// val managedItem = it.copyToRealmOrUpdate(this.model.item)
// if (managedItem is Savable) {
// val uniqueIdentifier = managedItem.id
// finishActivityWithResult(uniqueIdentifier)
// }
// }
onDataSaved() onDataSaved()
} }
else -> { else -> {

@ -51,10 +51,10 @@ class EditableDataActivity : MediaActivity() {
initUI() initUI()
} }
override fun onPause() { // override fun onPause() {
super.onPause() // super.onPause()
this.paApplication.backupOperator?.backupIfNecessary() // this.paApplication.backupOperator?.backupIfNecessary()
} // }
/** /**
* Init UI * Init UI

@ -11,13 +11,12 @@ import io.realm.RealmModel
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.writeAsync
open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegate { open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegate {
@ -68,25 +67,16 @@ open class EditableDataFragment : DataManagerFragment(), RowRepresentableDelegat
} }
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
(this.model.item as RowUpdatable).updateValue(value, row) getRealm().executeTransaction {
this.writeIfPossible(value, row)
rowRepresentableAdapter.refreshRow(row)
}
private fun writeIfPossible(value: Any?, row: RowRepresentable) {
if (!this.isUpdating) {
return
}
getRealm().writeAsync { asyncRealm ->
try { try {
asyncRealm.copyToRealmOrUpdate(this.model.item) (this.model.item as RowUpdatable).updateValue(value, row)
} catch (e: Exception) { } catch (e: Exception) {
CrashLogging.log("Exception caught: row = $row, value=$value, class=${this.javaClass}") CrashLogging.log("Exception caught: row = $row, value=$value, class=${this.javaClass}")
throw e throw e
} }
} }
rowRepresentableAdapter.refreshRow(row)
} }
/** /**

@ -13,8 +13,8 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -215,7 +215,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
when (row) { when (row) {
is Comment -> { is Comment -> {
if (row.isValidForDelete(getRealm())) { if (row.isValidForDelete(getRealm())) {
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
delay(300) delay(300)
showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = { showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = {
playerModel.deleteComment(row) playerModel.deleteComment(row)

@ -16,7 +16,6 @@ import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.isSameDay import net.pokeranalytics.android.util.extensions.isSameDay
import net.pokeranalytics.android.util.extensions.mediumDate import net.pokeranalytics.android.util.extensions.mediumDate
import net.pokeranalytics.android.util.extensions.writeAsync
import java.util.* import java.util.*
class PlayerDataViewModel : DataManagerViewModel(), StaticRowRepresentableDataSource { class PlayerDataViewModel : DataManagerViewModel(), StaticRowRepresentableDataSource {
@ -132,11 +131,9 @@ class PlayerDataViewModel : DataManagerViewModel(), StaticRowRepresentableDataSo
*/ */
fun cleanupComments() { // called when saving the custom field fun cleanupComments() { // called when saving the custom field
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val ids = this.commentsToDelete.map { it.id } this.commentsToDelete.forEach { // entries are out of realm
realm.writeAsync { asyncRealm -> realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
ids.forEach { id -> // entries are out of realm
asyncRealm.where<Comment>().equalTo("id", id).findFirst()?.deleteFromRealm()
} }
} }
realm.close() realm.close()

@ -2,8 +2,8 @@ package net.pokeranalytics.android.ui.modules.data
import android.content.Context import android.content.Context
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager
@ -141,7 +141,7 @@ class TransactionDataFragment : EditableDataFragment(), StaticRowRepresentableDa
when (val next = rows[index + 1]) { when (val next = rows[index + 1]) {
TransactionPropertiesRow.DATE, TransactionPropertiesRow.COMMENT -> {} TransactionPropertiesRow.DATE, TransactionPropertiesRow.COMMENT -> {}
else -> { else -> {
CoroutineScope(context = Dispatchers.Main).launch { GlobalScope.launch(Dispatchers.Main) {
delay(200) delay(200)
onRowSelected(0, next) onRowSelected(0, next)
} }

@ -39,13 +39,12 @@ import net.pokeranalytics.android.ui.modules.filter.FiltersActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.modules.session.SessionActivity import net.pokeranalytics.android.ui.modules.session.SessionActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.extensions.count import net.pokeranalytics.android.util.extensions.count
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
import java.util.* import java.util.*
class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseListener { class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseListener {
@ -495,14 +494,9 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
* Delete selected transaction * Delete selected transaction
*/ */
private fun deleteSelectedTransaction() { private fun deleteSelectedTransaction() {
getRealm().executeTransaction {
this.selectedTransaction?.id?.let { transactionId -> selectedTransaction?.deleteFromRealm()
getRealm().writeAsync { asyncRealm ->
val transaction = asyncRealm.findById<Transaction>(transactionId)
transaction?.deleteFromRealm()
}
} }
selectedTransactionPosition = -1 selectedTransactionPosition = -1
} }

@ -20,7 +20,9 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.SessionRowView import net.pokeranalytics.android.ui.view.SessionRowView
import net.pokeranalytics.android.util.extensions.getMonthAndYear import net.pokeranalytics.android.util.extensions.getMonthAndYear
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.HashMap
/** /**
@ -180,7 +182,7 @@ class FeedSessionRowRepresentableAdapter(
allSessions.clear() allSessions.clear()
allSessions.addAll(this.pendingSessions) allSessions.addAll(this.pendingSessions)
allSessions.addAll(this.startedSessions) allSessions.addAll(this.startedSessions)
// Timber.d("Update session list, total count = ${allSessions.size}") Timber.d("Update session list, total count = ${allSessions.size}")
val headersPositions = HashMap<Int, Date?>() val headersPositions = HashMap<Int, Date?>()
@ -208,7 +210,7 @@ class FeedSessionRowRepresentableAdapter(
} }
sortedHeaders = headersPositions.toSortedMap() sortedHeaders = headersPositions.toSortedMap()
// Timber.d("Create viewTypesPositions in: ${System.currentTimeMillis() - start}ms") Timber.d("Create viewTypesPositions in: ${System.currentTimeMillis() - start}ms")
} }
/** /**

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

@ -234,17 +234,17 @@ open class FilterDetailsFragment : RealmFragment(), RowRepresentableDelegate {
//TODO: Save currentFilter details data //TODO: Save currentFilter details data
Timber.d("Save data for queryWith: ${currentFilter?.id}") Timber.d("Save data for queryWith: ${currentFilter?.id}")
// this.model.selectedRows.forEach { this.model.selectedRows.forEach {
// Timber.d("Selected rows: $it") Timber.d("Selected rows: $it")
// } }
this.activityModel.selectedCategoryRow?.let { category -> this.activityModel.selectedCategoryRow?.let { category ->
// getRealm().executeTransaction { getRealm().executeTransaction {
currentFilter?.remove(category) currentFilter?.remove(category)
val validConditions = this.model.selectedRows.filter { it.queryCondition != null } val validConditions = this.model.selectedRows.filter { it.queryCondition != null }
currentFilter?.createOrUpdateFilterConditions(validConditions) currentFilter?.createOrUpdateFilterConditions(validConditions)
// } }
} }
currentFilter?.filterConditions?.forEach { currentFilter?.filterConditions?.forEach {

@ -4,12 +4,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.where
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
enum class FilterActivityRequestCode { enum class FilterActivityRequestCode {
SELECT_FILTER, SELECT_FILTER,
@ -46,9 +45,10 @@ interface FilterHandler {
fun saveFilter(context: Context, filterId: String) { fun saveFilter(context: Context, filterId: String) {
Preferences.setActiveFilterId(filterId, context) Preferences.setActiveFilterId(filterId, context)
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.writeAsync { asyncRealm -> realm.executeTransaction { executeRealm ->
currentFilter(context, asyncRealm)?.let { currentFilter(context, executeRealm)?.let {
it.useCount++ it.useCount++
} }
} }
@ -61,9 +61,8 @@ interface FilterHandler {
} }
fun currentFilter(context: Context, realm: Realm): Filter? { fun currentFilter(context: Context, realm: Realm): Filter? {
return Preferences.getActiveFilterId(context)?.let { id -> return Preferences.getActiveFilterId(context)?.let {
realm.findById(id) realm.where<Filter>().equalTo("id", it).findFirst()
// realm.where<Filter>().equalTo("id", it).findFirst()
} ?: run { } ?: run {
null null
} }

@ -15,12 +15,12 @@ import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.extensions.sorted import net.pokeranalytics.android.util.extensions.sorted
import net.pokeranalytics.android.util.extensions.writeAsync
open class FiltersFragment : RealmFragment(), RowRepresentableDelegate { open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
@ -218,13 +218,16 @@ open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
* Validate the updates of the queryWith * Validate the updates of the queryWith
*/ */
private fun validateUpdates() { private fun validateUpdates() {
val filter = this.model.currentFilter val currentFilter = this.model.currentFilter
filter?.let { currentFilter ->
getRealm().writeAsync { asyncRealm -> getRealm().executeTransaction { realm ->
asyncRealm.copyToRealmOrUpdate(currentFilter) currentFilter?.let {
it.name = it.query.getName(requireContext())
realm.copyToRealmOrUpdate(it)
} }
} }
val filterId = filter?.id ?: ""
val filterId = currentFilter?.id ?: ""
finishActivityWithResult(filterId) finishActivityWithResult(filterId)
} }
@ -236,9 +239,9 @@ open class FiltersFragment : RealmFragment(), RowRepresentableDelegate {
val filterCopy = this.model.filterCopy val filterCopy = this.model.filterCopy
val filterId = filterCopy?.id ?: "" val filterId = filterCopy?.id ?: ""
getRealm().writeAsync { asyncRealm -> getRealm().executeTransaction { realm ->
filterCopy?.let { filterCopy?.let {
asyncRealm.copyToRealmOrUpdate(it) realm.copyToRealmOrUpdate(it)
} }
} }
finishActivityWithResult(filterId) finishActivityWithResult(filterId)

@ -3,14 +3,11 @@ package net.pokeranalytics.android.ui.modules.filter
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.ui.modules.datalist.DataListFragment import net.pokeranalytics.android.ui.modules.datalist.DataListFragment
import net.pokeranalytics.android.ui.modules.filter.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI import net.pokeranalytics.android.ui.modules.filter.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
open class FiltersListFragment : DataListFragment() { open class FiltersListFragment : DataListFragment() {
@ -18,19 +15,12 @@ open class FiltersListFragment : DataListFragment() {
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
is Filter -> { is Filter -> {
val filterId = row.id getRealm().executeTransaction {
// val filter = row.realm.copyFromRealm(row) row.updateValue(value, row)
// row.updateValue(value, row) }
getRealm().writeAsync ({ asyncRealm ->
asyncRealm.findById<Filter>(filterId)?.let { filter ->
filter.updateValue(value, filter)
// asyncRealm.copyToRealmOrUpdate(filter)
} ?: throw PAIllegalStateException("missing filter: $filterId")
}, {
val index = this.model.items.indexOf(row) val index = this.model.items.indexOf(row)
this.dataListAdapter.notifyItemChanged(index) this.dataListAdapter.notifyItemChanged(index)
updateFilterUIIfNecessary(requireContext(), row.id) updateFilterUIIfNecessary(requireContext(), row.id)
} )
} }
else -> super.onRowValueChanged(value, row) else -> super.onRowValueChanged(value, row)
} }

@ -35,7 +35,6 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
import timber.log.Timber import timber.log.Timber
@ -204,8 +203,7 @@ class EditorFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListen
val playerId = data?.getStringExtra(BaseFragment.BundleKey.PRIMARY_KEY.value) val playerId = data?.getStringExtra(BaseFragment.BundleKey.PRIMARY_KEY.value)
?: throw PAIllegalStateException("Primary key not set where as activity has finished") ?: throw PAIllegalStateException("Primary key not set where as activity has finished")
getRealm().findById<Player>(playerId)?.let { player -> getRealm().findById<Player>(playerId)?.let { player ->
val unmanagedPlayer = player.realm.copyFromRealm(player) this.model.playerSelected(player)
this.model.playerSelected(unmanagedPlayer)
} ?: throw PAIllegalStateException("Player (id=$playerId) not found") } ?: throw PAIllegalStateException("Player (id=$playerId) not found")
this.editorAdapter.notifyDataSetChanged() this.editorAdapter.notifyDataSetChanged()
} }
@ -686,9 +684,10 @@ class EditorFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListen
*/ */
private fun deleteHand() { private fun deleteHand() {
getRealm().writeAsync { asyncRealm -> getRealm().findById<HandHistory>(this.model.handHistory.id)?.let { hh ->
val hh = asyncRealm.findById<HandHistory>(this.model.handHistory.id) getRealm().executeTransaction {
hh?.deleteFromRealm() hh.deleteFromRealm()
}
} }
this.activity?.finish() this.activity?.finish()

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

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

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

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

@ -592,6 +592,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
this.drawer.drawTable(canvas, context) this.drawer.drawTable(canvas, context)
frameHandler(bitmap, vo) frameHandler(bitmap, vo)
bitmap.recycle()
} }

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

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

@ -13,7 +13,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -28,7 +28,6 @@ import net.pokeranalytics.android.model.extensions.getState
import net.pokeranalytics.android.model.extensions.scheduleStopNotification import net.pokeranalytics.android.model.extensions.scheduleStopNotification
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.FavoriteSessionFinder import net.pokeranalytics.android.model.utils.FavoriteSessionFinder
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
@ -186,31 +185,29 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
if (sessionRealm != null) { if (sessionRealm != null) {
if (this.model.duplicate) { // duplicate session if (this.model.duplicate) { // duplicate session
realm.writeAsync { asyncRealm -> realm.executeTransaction {
asyncRealm.findById<Session>(sessionId)?.duplicate()?.let { duplicate -> val session = sessionRealm.duplicate()
currentSession = asyncRealm.copyFromRealm(duplicate) currentSession = session
}
// val session = sessionRealm.duplicate()
// currentSession = session
} }
sessionHasBeenUserCustomized = false sessionHasBeenUserCustomized = false
} else { // show existing session } else { // show existing session
currentSession = realm.copyFromRealm(sessionRealm) currentSession = sessionRealm
sessionHasBeenUserCustomized = true sessionHasBeenUserCustomized = true
} }
} else { } else {
throw PAIllegalStateException("Session cannot be null here, session id = $sessionId") throw PAIllegalStateException("Session cannot be null here, session id = $sessionId")
} }
} else { // create new session } else { // create new session
currentSession = Session.newInstance(realm, this.model.isTournament) realm.executeTransaction { executeRealm ->
FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, null, requireContext(), realm) currentSession = Session.newInstance(executeRealm, this.model.isTournament)
FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, null, requireContext())
}
// Find the nearest location around the user // Find the nearest location around the user
parentActivity?.findNearestLocation { parentActivity?.findNearestLocation {
it?.let { location -> it?.let { location ->
realm.writeAsync { asyncRealm -> realm.executeTransaction { executeRealm ->
val realmLocation = asyncRealm.findById<Location>(location.id) val realmLocation = executeRealm.findById<Location>(location.id)
FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, realmLocation, requireContext(), asyncRealm) FavoriteSessionFinder.copyParametersFromFavoriteSession(currentSession, realmLocation, requireContext())
currentSession.location = realmLocation currentSession.location = realmLocation
} }
@ -220,10 +217,6 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
sessionHasBeenUserCustomized = false sessionHasBeenUserCustomized = false
} }
// load everything and have some SessionManager links it all
//
} }
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
@ -256,8 +249,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
showBottomSheet(row, this, data, false, session.currency) showBottomSheet(row, this, data, false, session.currency)
} }
SessionPropertiesRow.HANDS -> { SessionPropertiesRow.HANDS -> {
val handHistories = getRealm().where(HandHistory::class.java).equalTo("session.id", session.id).findAll() val hhIds = session.handHistories?.map { it.id }?.toTypedArray()
val hhIds = handHistories?.map { it.id }?.toTypedArray()
DataListActivity.newInstance(this, LiveData.HAND_HISTORY, false, hhIds, false) DataListActivity.newInstance(this, LiveData.HAND_HISTORY, false, hhIds, false)
} }
else -> showBottomSheet(row, this, data, currentCurrency = session.currency) else -> showBottomSheet(row, this, data, currentCurrency = session.currency)
@ -267,8 +259,9 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
this.sessionHasBeenUserCustomized = true this.sessionHasBeenUserCustomized = true
try { try {
getRealm().executeTransaction {
this.currentSession.updateValue(value, row) this.currentSession.updateValue(value, row)
this.writeChanges() }
} catch (e: PAIllegalStateException) { } catch (e: PAIllegalStateException) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
return return
@ -286,25 +279,6 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
} }
private fun writeChanges() {
Timber.d("write changes")
getRealm().writeAsync ({ asyncRealm ->
Timber.d("start transaction")
asyncRealm.copyToRealmOrUpdate(this.currentSession)
Timber.d("yupyup")
}, { // onSuccess we retrieved the object because it might have been changed (i.e. session set added). Not retrieving it can for example copy it without its sessionset, writing null in db whereas it should have it
Timber.d("onSuccess")
val id = this.currentSession.id
getRealm().refresh()
getRealm().findById<Session>(id)?.let { session ->
this.currentSession = getRealm().copyFromRealm(session)
Timber.d("Session retrieved, set = ${this.currentSession.sessionSet}, br = ${session.bankroll}")
} ?: throw PAIllegalStateException("session not found")
})
}
/** /**
* Update the UI with the session data * Update the UI with the session data
* Should be called after the initialization of the session * Should be called after the initialization of the session
@ -419,14 +393,11 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
} }
currentSession.startOrContinue() currentSession.startOrContinue()
this.writeChanges()
binding.recyclerView.smoothScrollToPosition(0) binding.recyclerView.smoothScrollToPosition(0)
} }
SessionState.STARTED -> { SessionState.STARTED -> {
currentSession.pause() currentSession.pause()
this.writeChanges()
} }
else -> { else -> {
} }
@ -434,7 +405,6 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
updateSessionUI() updateSessionUI()
} }
private fun computeOptimalDuration() { private fun computeOptimalDuration() {
Timber.d("Start optimal duration finding attempt...") Timber.d("Start optimal duration finding attempt...")
@ -444,7 +414,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
var optimalDuration: Double? = null var optimalDuration: Double? = null
val cr = CoroutineScope(context = Dispatchers.IO).async { val cr = GlobalScope.async {
optimalDuration = CashGameOptimalDurationCalculator.start(isLive) optimalDuration = CashGameOptimalDurationCalculator.start(isLive)
} }
cr.await() cr.await()
@ -456,7 +426,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
currentSession.scheduleStopNotification(requireContext(), delay) currentSession.scheduleStopNotification(requireContext(), delay)
val formattedDuration = (it / 3600 / 1000).formattedHourlyDuration() val formattedDuration = (it / 3600 / 1000).formattedHourlyDuration()
// Timber.d("Setting stop notification in: $formattedDuration") Timber.d("Setting stop notification in: $formattedDuration")
val message = requireContext().getString(R.string.stop_notification_in_, formattedDuration) val message = requireContext().getString(R.string.stop_notification_in_, formattedDuration)
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
} }
@ -512,17 +482,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
val bankrollId = this.currentSession.bankroll?.id val bankrollId = this.currentSession.bankroll?.id
val id = currentSession.id this.currentSession.delete()
getRealm().writeAsync ({ asyncRealm ->
asyncRealm.findById<Session>(id)?.let { session ->
session.cleanup()
session.deleteFromRealm()
} ?: throw PAIllegalStateException("session not found")
}, {
Timber.d("delete successful")
// getRealm().refresh()
})
bankrollId?.let { bankrollId?.let {
BankrollReportManager.notifyBankrollReportImpact(bankrollId) BankrollReportManager.notifyBankrollReportImpact(bankrollId)
} }
@ -535,11 +495,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun onBackPressed() { override fun onBackPressed() {
super.onBackPressed() super.onBackPressed()
if (!sessionHasBeenUserCustomized) { if (!sessionHasBeenUserCustomized) {
currentSession.delete()
val id = currentSession.id
getRealm().writeAsync { asyncRealm ->
asyncRealm.findById<Session>(id)?.delete()
}
} }
} }
@ -570,7 +526,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
} }
override fun charSequenceForRow(row: RowRepresentable, context: Context): String { override fun charSequenceForRow(row: RowRepresentable, context: Context): String {
return this.currentSession.charSequenceForRow(row, context, getRealm()) return this.currentSession.charSequenceForRow(row, context)
} }
override fun actionIconForRow(row: RowRepresentable): Int? { override fun actionIconForRow(row: RowRepresentable): Int? {
@ -637,18 +593,18 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
mapOf( mapOf(
"bb" to session.cgBiggestBet, "bb" to session.cgBiggestBet,
"fee" to session.tournamentEntryFee, "fee" to session.tournamentEntryFee,
"ratedBuyin" to session.buyin "ratedBuyin" to session.result?.buyin
) )
) )
SessionPropertiesRow.BREAK_TIME -> row.editingDescriptors(mapOf()) SessionPropertiesRow.BREAK_TIME -> row.editingDescriptors(mapOf())
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> row.editingDescriptors( SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> row.editingDescriptors(
mapOf( mapOf(
"defaultValue" to session.cashout "defaultValue" to session.result?.cashout
) )
) )
SessionPropertiesRow.NET_RESULT -> row.editingDescriptors( SessionPropertiesRow.NET_RESULT -> row.editingDescriptors(
mapOf( mapOf(
"defaultValue" to session.netResult "defaultValue" to session.result?.netResult
) )
) )
SessionPropertiesRow.COMMENT -> row.editingDescriptors( SessionPropertiesRow.COMMENT -> row.editingDescriptors(
@ -668,7 +624,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
) )
SessionPropertiesRow.POSITION -> row.editingDescriptors( SessionPropertiesRow.POSITION -> row.editingDescriptors(
mapOf( mapOf(
"defaultValue" to session.tournamentFinalPosition "defaultValue" to session.result?.tournamentFinalPosition
) )
) )
SessionPropertiesRow.HANDS_COUNT -> row.editingDescriptors( SessionPropertiesRow.HANDS_COUNT -> row.editingDescriptors(
@ -685,7 +641,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
mapOf( mapOf(
"sb" to session.cgBiggestBet?.round(), "sb" to session.cgBiggestBet?.round(),
"bb" to session.cgBiggestBet?.round(), "bb" to session.cgBiggestBet?.round(),
"tips" to session.tips "tips" to session.result?.tips
) )
) )
is CustomField -> { is CustomField -> {
@ -707,6 +663,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun resultCaptureTypeSelected(resultCaptureType: ResultCaptureType, applyBankroll: Boolean) { override fun resultCaptureTypeSelected(resultCaptureType: ResultCaptureType, applyBankroll: Boolean) {
getRealm().executeTransaction { // cleanup existing results
when (resultCaptureType) { when (resultCaptureType) {
ResultCaptureType.NET_RESULT -> { ResultCaptureType.NET_RESULT -> {
this.currentSession.clearBuyinCashedOut() this.currentSession.clearBuyinCashedOut()
@ -715,7 +672,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
this.currentSession.clearNetResult() this.currentSession.clearNetResult()
} }
} }
this.writeChanges() }
this.model.resultCaptureType = resultCaptureType this.model.resultCaptureType = resultCaptureType
if (applyBankroll) { if (applyBankroll) {

@ -17,6 +17,7 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.SeparatorRow import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.extensions.sorted import net.pokeranalytics.android.util.extensions.sorted
import java.util.ArrayList
class SessionViewModel : ViewModel() { class SessionViewModel : ViewModel() {
@ -51,7 +52,7 @@ class SessionViewModel : ViewModel() {
fun updatedRowRepresentationForCurrentState(session: Session, realm: Realm, context: Context) { fun updatedRowRepresentationForCurrentState(session: Session, realm: Realm, context: Context) {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
// val result = session.result val result = session.result
val currency = session.currency val currency = session.currency
// Headers // Headers
@ -61,7 +62,7 @@ class SessionViewModel : ViewModel() {
CustomizableRowRepresentable( CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG, RowViewType.HEADER_TITLE_AMOUNT_BIG,
title = session.getFormattedDuration(), title = session.getFormattedDuration(),
valueTextFormat = ComputedStat(Stat.NET_RESULT, session.net, currency = session.currency).textFormat valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = session.currency).textFormat
) )
) )
rows.add(SeparatorRow()) rows.add(SeparatorRow())
@ -71,7 +72,7 @@ class SessionViewModel : ViewModel() {
CustomizableRowRepresentable( CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG, RowViewType.HEADER_TITLE_AMOUNT_BIG,
resId = R.string.pause, resId = R.string.pause,
valueTextFormat = ComputedStat(Stat.NET_RESULT, session.net, currency = currency).textFormat valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat
) )
) )
rows.add(SeparatorRow()) rows.add(SeparatorRow())
@ -81,7 +82,7 @@ class SessionViewModel : ViewModel() {
CustomizableRowRepresentable( CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG, RowViewType.HEADER_TITLE_AMOUNT_BIG,
title = session.getFormattedDuration(), title = session.getFormattedDuration(),
valueTextFormat = ComputedStat(Stat.NET_RESULT, session.net, currency = currency).textFormat valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat
) )
) )
rows.add( rows.add(
@ -99,14 +100,11 @@ class SessionViewModel : ViewModel() {
} }
// Rows // Rows
rows.addAll(SessionPropertiesRow.getRows(session, this.resultCaptureType, context, realm)) rows.addAll(SessionPropertiesRow.getRows(session, this.resultCaptureType, context))
// Add custom fields // Add custom fields
rows.add(SeparatorRow()) rows.add(SeparatorRow())
rows.addAll(realm.sorted<CustomField>())
val customFields = realm.sorted<CustomField>()
val unmanaged = customFields.map { realm.copyFromRealm(it) }
rows.addAll(unmanaged)
this.rows = rows this.rows = rows
} }

@ -4,13 +4,11 @@ import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import kotlinx.android.synthetic.main.fragment_dealt_hands_config.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentDealtHandsConfigBinding import net.pokeranalytics.android.databinding.FragmentDealtHandsConfigBinding
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.UserConfig import net.pokeranalytics.android.model.realm.UserConfig
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.extensions.writeAsync
class DealtHandsPerHourFragment : RealmFragment() { class DealtHandsPerHourFragment : RealmFragment() {
@ -51,34 +49,34 @@ class DealtHandsPerHourFragment : RealmFragment() {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
val userConfig = UserConfig.getConfiguration(this.getRealm()) val userConfig = UserConfig.getConfiguration(this.getRealm())
this.liveValue.hint = "${userConfig.liveDealtHandsPerHour}" this.binding.liveValue.hint = "${userConfig.liveDealtHandsPerHour}"
this.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}" this.binding.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}"
} }
private fun save() { private fun save() {
getRealm().writeAsync { asyncRealm -> getRealm().executeTransaction { realm ->
val userConfig = UserConfig.getConfiguration(asyncRealm) val userConfig = UserConfig.getConfiguration(realm)
this.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour -> this.binding.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour ->
userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour
} }
this.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour -> this.binding.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour ->
userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour
} }
asyncRealm.copyToRealmOrUpdate(userConfig) realm.copyToRealmOrUpdate(userConfig)
// update all precomputed hand counts // update all precomputed hand counts
asyncRealm.where(ComputableResult::class.java).findAll().forEach { cr -> realm.where(ComputableResult::class.java).findAll().forEach { cr ->
cr.session?.let { session -> cr.session?.let { session ->
cr.estimatedHands = session.estimatedHands cr.estimatedHands = session.estimatedHands
} }
} }
} }
this.liveValue.clearFocus() this.binding.liveValue.clearFocus()
this.onlineValue.clearFocus() this.binding.onlineValue.clearFocus()
// Hides keyboard // Hides keyboard
val imm: InputMethodManager = val imm: InputMethodManager =

@ -16,7 +16,6 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.writeAsync
class TransactionFilterFragment : RealmFragment(), StaticRowRepresentableDataSource, class TransactionFilterFragment : RealmFragment(), StaticRowRepresentableDataSource,
RowRepresentableDelegate { RowRepresentableDelegate {
@ -97,13 +96,10 @@ class TransactionFilterFragment : RealmFragment(), StaticRowRepresentableDataSou
} }
private fun save() { private fun save() {
val ids = this.model.selectedTransactionTypes.map { it.id } getRealm().executeTransaction { realm ->
val userConfig = UserConfig.getConfiguration(realm)
// this.model.selectedTransactionTypes.joinToString(UUID_SEPARATOR) { it.id } userConfig.setTransactionTypeIds(this.model.selectedTransactionTypes)
getRealm().writeAsync { asyncRealm -> realm.copyToRealmOrUpdate(userConfig)
val userConfig = UserConfig.getConfiguration(asyncRealm)
userConfig.setTransactionTypeIds(ids.toSet())
asyncRealm.copyToRealmOrUpdate(userConfig)
} }
this.activity?.finish() this.activity?.finish()
} }

@ -18,7 +18,6 @@ import com.github.mikephil.charting.data.*
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.cell_calendar_time_unit.view.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedStat import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
@ -511,8 +510,9 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
if (row is Session) { if (row is Session) {
itemView.findViewById<AppCompatTextView>(R.id.gameResult)?.let { gameResult -> itemView.findViewById<AppCompatTextView>(R.id.gameResult)?.let { gameResult ->
val result = row.result?.net ?: 0.0
val formattedStat = val formattedStat =
ComputedStat(Stat.NET_RESULT, row.net, currency = row.currency).textFormat ComputedStat(Stat.NET_RESULT, result, currency = row.currency).textFormat
gameResult.setTextFormat(formattedStat, itemView.context) gameResult.setTextFormat(formattedStat, itemView.context)
} }
@ -689,7 +689,8 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
if (row is CellResult) { if (row is CellResult) {
itemView.timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background) val timeUnit = itemView.findViewById<View>(R.id.timeUnit)
timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background)
} }
} }

@ -147,8 +147,10 @@ class SessionRowView : FrameLayout {
this.infoIcon.isVisible = false this.infoIcon.isVisible = false
this.infoTitle.isVisible = false this.infoTitle.isVisible = false
val stat = ComputedStat(Stat.NET_RESULT, session.net, currency = session.currency) session.result?.net?.let { netResult ->
val stat = ComputedStat(Stat.NET_RESULT, netResult, currency = session.currency)
this.gameResult.setTextFormat(stat.textFormat, context) this.gameResult.setTextFormat(stat.textFormat, context)
}
// val formattedStat = ComputedStat(Stat.NET_RESULT, result, currency = session.currency).format() // val formattedStat = ComputedStat(Stat.NET_RESULT, result, currency = session.currency).format()
} }

@ -3,11 +3,11 @@ package net.pokeranalytics.android.ui.view.keyboard
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnection
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import kotlinx.android.synthetic.main.view_keyboard_stakes.view.* import net.pokeranalytics.android.databinding.ViewKeyboardStakesBinding
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.util.BLIND_SEPARATOR import net.pokeranalytics.android.util.BLIND_SEPARATOR
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
@ -16,6 +16,9 @@ class StakesKeyboardView : LinearLayoutCompat {
var inputConnection: InputConnection? = null var inputConnection: InputConnection? = null
private var _binding: ViewKeyboardStakesBinding? = null
private val binding get() = _binding!!
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
init(context, null) init(context, null)
} }
@ -30,42 +33,51 @@ class StakesKeyboardView : LinearLayoutCompat {
private fun init(context: Context, attrs: AttributeSet?) { private fun init(context: Context, attrs: AttributeSet?) {
val layoutInflater = LayoutInflater.from(context) val layoutInflater = LayoutInflater.from(context)
val view = layoutInflater.inflate(R.layout.view_keyboard_stakes, this, false) // val view = layoutInflater.inflate(R.layout.view_keyboard_stakes, this, false)
view.value_0.text = "0" _binding = ViewKeyboardStakesBinding.inflate(layoutInflater, this, true)
view.value_1.text = "1"
view.value_2.text = "2" binding.value0.text = "0"
view.value_3.text = "3" binding.value1.text = "1"
view.value_4.text = "4" binding.value2.text = "2"
view.value_5.text = "5" binding.value3.text = "3"
view.value_6.text = "6" binding.value4.text = "4"
view.value_7.text = "7" binding.value5.text = "5"
view.value_8.text = "8" binding.value6.text = "6"
view.value_9.text = "9" binding.value7.text = "7"
view.value_decimal.text = DecimalFormatSymbols.getInstance().decimalSeparator.toString() binding.value8.text = "8"
view.value_back.text = "" binding.value9.text = "9"
view.value_separator.text = "/" binding.valueDecimal.text = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
binding.valueBack.text = ""
view.value_0.setOnClickListener { this.commitText("0") } binding.valueSeparator.text = "/"
view.value_1.setOnClickListener { this.commitText("1") }
view.value_2.setOnClickListener { this.commitText("2") } binding.value0.setOnClickListener { this.commitText("0") }
view.value_3.setOnClickListener { this.commitText("3") } binding.value1.setOnClickListener { this.commitText("1") }
view.value_4.setOnClickListener { this.commitText("4") } binding.value2.setOnClickListener { this.commitText("2") }
view.value_5.setOnClickListener { this.commitText("5") } binding.value3.setOnClickListener { this.commitText("3") }
view.value_6.setOnClickListener { this.commitText("6") } binding.value4.setOnClickListener { this.commitText("4") }
view.value_7.setOnClickListener { this.commitText("7") } binding.value5.setOnClickListener { this.commitText("5") }
view.value_8.setOnClickListener { this.commitText("8") } binding.value6.setOnClickListener { this.commitText("6") }
view.value_9.setOnClickListener { this.commitText("9") } binding.value7.setOnClickListener { this.commitText("7") }
view.value_decimal.setOnClickListener { this.commitText(DecimalFormatSymbols.getInstance().decimalSeparator.toString()) } binding.value8.setOnClickListener { this.commitText("8") }
view.value_separator.setOnClickListener { this.commitText(BLIND_SEPARATOR) } binding.value9.setOnClickListener { this.commitText("9") }
view.value_back.setOnClickListener { this.deleteText() } binding.valueDecimal.setOnClickListener { this.commitText(DecimalFormatSymbols.getInstance().decimalSeparator.toString()) }
binding.valueSeparator.setOnClickListener { this.commitText(BLIND_SEPARATOR) }
binding.valueBack.setOnClickListener { this.deleteText() }
val layoutParams = FrameLayout.LayoutParams( val layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT FrameLayout.LayoutParams.WRAP_CONTENT
) )
addView(view, layoutParams) // addView(binding, layoutParams)
// addView(view, layoutParams)
}
fun setSeparatorVisibility(visible: Boolean) {
binding.valueSeparator.visibility = if (visible) View.VISIBLE else View.GONE
} }
private fun commitText(string: String) { private fun commitText(string: String) {

@ -2,7 +2,6 @@ package net.pokeranalytics.android.ui.view.rows
import android.content.Context import android.content.Context
import android.text.InputType import android.text.InputType
import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
@ -10,7 +9,6 @@ import net.pokeranalytics.android.model.extensions.SessionState
import net.pokeranalytics.android.model.extensions.getState import net.pokeranalytics.android.model.extensions.getState
import net.pokeranalytics.android.model.realm.ResultCaptureType import net.pokeranalytics.android.model.realm.ResultCaptureType
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
@ -50,7 +48,7 @@ enum class SessionPropertiesRow : RowRepresentable {
/** /**
* Return the rows to display for the current session state * Return the rows to display for the current session state
*/ */
fun getRows(session: Session, resultCaptureType: ResultCaptureType?, context: Context, realm: Realm): List<RowRepresentable> { fun getRows(session: Session, resultCaptureType: ResultCaptureType?, context: Context): List<RowRepresentable> {
val state = session.getState() val state = session.getState()
when (session.type) { when (session.type) {
Session.Type.TOURNAMENT.ordinal -> { Session.Type.TOURNAMENT.ordinal -> {
@ -79,8 +77,6 @@ enum class SessionPropertiesRow : RowRepresentable {
TIPS)) TIPS))
fields.add(SeparatorRow()) fields.add(SeparatorRow())
fields.add(COMMENT) fields.add(COMMENT)
if (session.handHistories?.isNotEmpty() == true) { if (session.handHistories?.isNotEmpty() == true) {
fields.add(HANDS) fields.add(HANDS)
} }
@ -137,9 +133,7 @@ enum class SessionPropertiesRow : RowRepresentable {
// PROPERTIES // PROPERTIES
fields.add(SeparatorRow()) fields.add(SeparatorRow())
fields.add(COMMENT) fields.add(COMMENT)
if (session.handHistories?.isNotEmpty() == true) {
val handHistories = realm.where(HandHistory::class.java).equalTo("session.id", session.id).findAll()
if (handHistories?.isNotEmpty() == true) {
fields.add(HANDS) fields.add(HANDS)
} }
fields.add(SeparatorRow()) fields.add(SeparatorRow())

@ -3,7 +3,6 @@ package net.pokeranalytics.android.ui.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException
@ -212,32 +211,17 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
} }
return null return null
} }
BottomSheetType.MULTI_SELECTION -> this.selectedRowsRealmCopy() BottomSheetType.MULTI_SELECTION -> this.selectedRows
BottomSheetType.NUMERIC_TEXT -> this.doubleValue BottomSheetType.NUMERIC_TEXT -> this.doubleValue
BottomSheetType.GRID -> this.defaultSize BottomSheetType.GRID -> this.defaultSize
BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> this.someValues BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> this.someValues
BottomSheetType.LIST_STATIC -> this.selectedRowsRealmCopy().firstOrNull() BottomSheetType.LIST_STATIC -> this.selectedRows.firstOrNull()
BottomSheetType.SUM -> this.doubleValue BottomSheetType.SUM -> this.doubleValue
BottomSheetType.CASH_GAME_STAKES -> Stakes(this.secondStringValue, this.ante) BottomSheetType.CASH_GAME_STAKES -> Stakes(this.secondStringValue, this.ante)
else -> null else -> null
} }
} }
private fun selectedRowsRealmCopy(): List<*> {
val realmObjects = this.selectedRows.filterIsInstance<RealmObject>()
return if (realmObjects.isNotEmpty()) {
realmObjects.map { obj ->
if (obj.isManaged) {
obj.realm.copyFromRealm(obj)
} else {
obj
}
}
} else {
this.selectedRows
}
}
fun isSelected(row: RowRepresentable): Boolean { fun isSelected(row: RowRepresentable): Boolean {
return this.selectedRows.contains(row) return this.selectedRows.contains(row)
} }
@ -259,17 +243,9 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
} }
fun rowSelected(position: Int): RowRepresentable? { fun rowSelected(position: Int): RowRepresentable? {
when(this.row.bottomSheetType) { return when(this.row.bottomSheetType) {
BottomSheetType.LIST -> { BottomSheetType.LIST -> this.realmData?.get(position)
this.realmData?.realm?.let { realm -> BottomSheetType.LIST_STATIC -> this.staticRows[position]
val item = this.realmData?.get(position) as? RealmObject
item?.let {
return realm.copyFromRealm(it) as? RowRepresentable
}
}
return null
}
BottomSheetType.LIST_STATIC -> return this.staticRows[position]
else -> throw PAIllegalStateException("row selected for unmanaged bottom sheet type") else -> throw PAIllegalStateException("row selected for unmanaged bottom sheet type")
} }
} }

@ -1,26 +1,26 @@
package net.pokeranalytics.android.util package net.pokeranalytics.android.util
import android.content.Context import android.content.Context
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope import net.pokeranalytics.android.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.api.BackupApi
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.util.csv.ProductCSVDescriptors import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.concurrent.TimeUnit
class BackupOperator(var context: Context) { class BackupOperator(var context: Context) {
private var sessions: RealmResults<Session>? = null private var sessions: RealmResults<Session>? = null
private var transactions: RealmResults<Transaction>? = null private var transactions: RealmResults<Transaction>? = null
private var sessionsChanged = false private var sessionsInitialized = false
private var transactionsChanged = false private var transactionsInitialized = false
private val realm = Realm.getDefaultInstance() private val realm = Realm.getDefaultInstance()
@ -28,59 +28,46 @@ class BackupOperator(var context: Context) {
this.sessions = this.realm.where(Session::class.java).findAllAsync() this.sessions = this.realm.where(Session::class.java).findAllAsync()
this.sessions?.addChangeListener { _ -> this.sessions?.addChangeListener { _ ->
sessionsChanged = true if (this.sessionsInitialized) {
Preferences.getBackupEmail(context)?.let {
backupDataType(DataType.SESSION)
}
}
this.sessionsInitialized = true
} }
this.transactions = this.realm.where(Transaction::class.java).findAllAsync() this.transactions = this.realm.where(Transaction::class.java).findAllAsync()
this.transactions?.addChangeListener { _ -> this.transactions?.addChangeListener { _ ->
transactionsChanged = true if (this.transactionsInitialized) {
Preferences.getBackupEmail(context)?.let {
backupDataType(DataType.TRANSACTION)
} }
} }
this.transactionsInitialized = true
fun backupIfNecessary() {
// Timber.d(">>> backupIfNecessary")
Preferences.getBackupEmail(context)?.let { email ->
this.backupSessionsIfNecessary(email)
this.backupTransactionsIfNecessary(email)
} }
} }
private fun backupSessionsIfNecessary(email: String) { private fun backupDataType(dataType: DataType) {
val data = Data.Builder()
.putInt(BackupWorker.ParamKeys.DATA.value, dataType.ordinal)
if (this.sessionsChanged) { var duration = 10L
Timber.d(">>>> backup sessions") var unit = TimeUnit.HOURS
val sessions = this.realm.where(Session::class.java).findAll().sort("startDate") if (BuildConfig.DEBUG) {
val csv = ProductCSVDescriptors.pokerAnalyticsAndroid6Sessions.toCSV(sessions) duration = 1L
val fileName = "sessions_${Date().dateTimeFileFormatted}.csv" unit = TimeUnit.SECONDS
CoroutineScope(context = Dispatchers.IO).launch {
if (BackupApi.backupFile(context, email, fileName, csv)) {
sessionsChanged = false
}
} }
}
}
private fun backupTransactionsIfNecessary(email: String) {
if (this.transactionsChanged) { val backupWorker = OneTimeWorkRequestBuilder<BackupWorker>()
Timber.d(">>>> backup transactions") .setInitialDelay(duration, unit)
val transactions = this.realm.where(Transaction::class.java).findAll().sort("date") .setInputData(data.build())
val csv = ProductCSVDescriptors.pokerAnalyticsAndroidTransactions.toCSV(transactions) .addTag(dataType.workId)
val fileName = "transactions_${Date().dateTimeFileFormatted}.csv" .build()
CoroutineScope(context = Dispatchers.IO).launch { Timber.d(">>> create backupTask")
if (BackupApi.backupFile(context, email, fileName, csv)) {
transactionsChanged = false
}
}
}
WorkManager.getInstance(context).enqueueUniqueWork(dataType.workId, ExistingWorkPolicy.REPLACE, backupWorker)
} }
} }

@ -0,0 +1,86 @@
package net.pokeranalytics.android.util
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.api.BackupApi
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.csv.ProductCSVDescriptors
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber
import java.util.*
class BackupWorker(var context: Context, var params: WorkerParameters) : Worker(context, params) {
enum class ParamKeys(val value: String) {
DATA("title"),
}
override fun doWork(): Result {
val data = params.inputData
val dataTypeInt = data.getInt(ParamKeys.DATA.value, 0)
val dataType = DataType.values()[dataTypeInt]
Preferences.getBackupEmail(context)?.let { email ->
val task = BackupTask(dataType, email, context)
task.start()
}
return Result.success()
}
}
class BackupTask(val dataType: DataType, val email: String, val context: Context) {
fun start() {
when(this.dataType) {
DataType.SESSION -> {
backupSessions()
}
DataType.TRANSACTION -> {
backupTransactions()
}
}
}
private fun backupSessions() {
Timber.d(">>>> backup sessions")
val realm = Realm.getDefaultInstance()
val sessions = realm.where(Session::class.java).findAll().sort("startDate")
val csv = ProductCSVDescriptors.pokerAnalyticsAndroid6Sessions.toCSV(sessions)
val fileName = "sessions_${Date().dateTimeFileFormatted}.csv"
CoroutineScope(context = Dispatchers.IO).launch {
val success = BackupApi.backupFile(context, email, fileName, csv)
Preferences.setSessionsBackupSuccess(success, context)
}
realm.close()
}
private fun backupTransactions() {
Timber.d(">>>> backup transactions")
val realm = Realm.getDefaultInstance()
val transactions = realm.where(Transaction::class.java).findAll().sort("date")
val csv = ProductCSVDescriptors.pokerAnalyticsAndroidTransactions.toCSV(transactions)
val fileName = "transactions_${Date().dateTimeFileFormatted}.csv"
CoroutineScope(context = Dispatchers.IO).launch {
val success = BackupApi.backupFile(context, email, fileName, csv)
Preferences.setTransactionsBackupSuccess(success, context)
}
realm.close()
}
}

@ -34,6 +34,14 @@ class FakeDataManager {
val bankroll = realm.where<Bankroll>().findAll().firstOrNull() val bankroll = realm.where<Bankroll>().findAll().firstOrNull()
val locations = realm.where<Location>().findAll() val locations = realm.where<Location>().findAll()
if (locations.size == 0) {
realm.executeTransaction {
listOf("Bellagio", "Aria", "Borgata").map {
realm.getOrCreate<Location>(it)
}
}
}
// Test endedSessions // Test endedSessions
Timber.d("*** Start creating ${numberOfSessions} fake computables...") Timber.d("*** Start creating ${numberOfSessions} fake computables...")
@ -42,16 +50,10 @@ class FakeDataManager {
realm.beginTransaction() realm.beginTransaction()
if (locations.size == 0) {
listOf("Bellagio", "Aria", "Borgata").map {
realm.getOrCreate<Location>(it)
}
}
for (index in 1..numberOfSessions) { for (index in 1..numberOfSessions) {
if (index % commitFrequency == 0) { if (index % commitFrequency == 0) {
Timber.d("****** committing at ${index} sessions...") Timber.d("****** committing at ${index} computables...")
realm.commitTransaction() realm.commitTransaction()
realm.beginTransaction() realm.beginTransaction()
} }
@ -80,8 +82,10 @@ class FakeDataManager {
session.tableSize = (2..10).random() session.tableSize = (2..10).random()
val buyin = buyinList.random() val buyin = buyinList.random()
session.buyin = buyinList.random() session.result?.let { result ->
session.cashout = resultsList.random() + buyin result.buyin = buyinList.random()
result.cashout = resultsList.random() + buyin
}
if (isTournament) { if (isTournament) {
session.tournamentEntryFee = buyin session.tournamentEntryFee = buyin
@ -95,8 +99,6 @@ class FakeDataManager {
// session.cgSmallBlind = bigBlind / 2.0 // session.cgSmallBlind = bigBlind / 2.0
} }
realm.copyToRealmOrUpdate(session)
} }
realm.commitTransaction() realm.commitTransaction()

@ -175,13 +175,14 @@ class LocationManager(private var context: Context) {
var locationResultReceived = false var locationResultReceived = false
val locationCallback = object : LocationCallback() { val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult) super.onLocationResult(locationResult)
locationResult?.locations?.let { locationResult.locations.firstOrNull()?.let { location ->
if (!locationResultReceived && it.isNotEmpty()) { if (!locationResultReceived) {
locationResultReceived = true locationResultReceived = true
callback(it.first()) callback(location)
fusedLocationClient.removeLocationUpdates(this) fusedLocationClient.removeLocationUpdates(this)
} }
} }
@ -194,7 +195,12 @@ class LocationManager(private var context: Context) {
locationRequest.interval = 200L locationRequest.interval = 200L
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper()) Looper.myLooper()?.let { looper ->
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, looper)
} ?: run {
callback.invoke(null)
}
} else { } else {
// If we don't have the permission, return null // If we don't have the permission, return null
callback.invoke(null) callback.invoke(null)

@ -33,7 +33,7 @@ class Preferences {
PATCH_SESSION_SETS("patchSessionSet"), PATCH_SESSION_SETS("patchSessionSet"),
PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"), PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"),
// PATCH_BLINDS_FORMAT("patchBlindFormat"), // PATCH_BLINDS_FORMAT("patchBlindFormat"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions"), PATCH_COMPUTABLE_RESULTS("patchPositiveSessions_v2"),
PATCH_ZERO_TABLE("patchZeroTable"), PATCH_ZERO_TABLE("patchZeroTable"),
SHOW_STOP_NOTIFICATIONS("showStopNotifications"), SHOW_STOP_NOTIFICATIONS("showStopNotifications"),
ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"), ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"),
@ -51,7 +51,9 @@ class Preferences {
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"), LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"),
PATCH_RATED_AMOUNT("patchRatedAmount[new field]"), PATCH_RATED_AMOUNT("patchRatedAmount[new field]"),
BACKUP_EMAIL("backupEmail"), BACKUP_EMAIL("backupEmail"),
LANGUAGE_CODE("languageCode") LANGUAGE_CODE("languageCode"),
SESSIONS_BACKUP_SUCCESS("sessionsBackupSuccess"),
TRANSACTIONS_BACKUP_SUCCESS("transactionsBackupSuccess")
} }
enum class FeedMessage { enum class FeedMessage {
@ -352,6 +354,21 @@ class Preferences {
return getString(Keys.LANGUAGE_CODE, context) return getString(Keys.LANGUAGE_CODE, context)
} }
fun setTransactionsBackupSuccess(success: Boolean, context: Context) {
setBoolean(Keys.TRANSACTIONS_BACKUP_SUCCESS, success, context)
}
fun transactionsBackupSuccess(context: Context): Boolean {
return getBoolean(Keys.TRANSACTIONS_BACKUP_SUCCESS, context, true)
}
fun setSessionsBackupSuccess(success: Boolean, context: Context) {
setBoolean(Keys.SESSIONS_BACKUP_SUCCESS, success, context)
}
fun sessionsBackupSuccess(context: Context): Boolean {
return getBoolean(Keys.SESSIONS_BACKUP_SUCCESS, context, true)
}
} }
} }

@ -7,7 +7,6 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.ObjectIdentifier import net.pokeranalytics.android.model.interfaces.ObjectIdentifier
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
/** /**
@ -71,8 +70,8 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
if (realm.isInTransaction) { if (realm.isInTransaction) {
this.deleteInsertedFromRealm(realm) this.deleteInsertedFromRealm(realm)
} else { } else {
realm.writeAsync { asyncRealm -> realm.executeTransaction {
this.deleteInsertedFromRealm(asyncRealm) this.deleteInsertedFromRealm(realm)
} }
} }

@ -26,7 +26,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
vararg elements: CSVField) vararg elements: CSVField)
: DataCSVDescriptor<T>(source, *elements) { : DataCSVDescriptor<T>(source, *elements) {
private var noSessionImport: Boolean = false var noSessionImport: Boolean = false
init { init {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -46,7 +46,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
protected fun parseSession(realm: Realm, record: CSVRecord, context: Context): Session? { protected fun parseSession(realm: Realm, record: CSVRecord, context: Context): Session? {
val isTournament = isTournament ?: false val isTournament = isTournament ?: false
val session = Session.newInstance(realm, isTournament) val session = Session.newInstance(realm, isTournament, managed = false)
var startDate: Date? = null var startDate: Date? = null
var endDate: Date? = null var endDate: Date? = null
@ -127,13 +127,13 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
} }
is SessionField.Buyin -> { is SessionField.Buyin -> {
val buyin = field.parse(value) val buyin = field.parse(value)
session.buyin = buyin session.result?.buyin = buyin
if (session.type == Session.Type.TOURNAMENT.ordinal) { if (session.type == Session.Type.TOURNAMENT.ordinal) {
session.tournamentEntryFee = buyin session.tournamentEntryFee = buyin
} else {} } else {}
} }
is SessionField.CashedOut -> session.cashout = field.parse(value) is SessionField.CashedOut -> session.result?.cashout = field.parse(value)
is SessionField.NetResult -> session.netResult = field.parse(value) is SessionField.NetResult -> session.result?.netResult = field.parse(value)
is SessionField.SessionType -> { is SessionField.SessionType -> {
Session.Type.getValueFromString(value)?.let { type -> Session.Type.getValueFromString(value)?.let { type ->
session.type = type.ordinal session.type = type.ordinal
@ -143,7 +143,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
is SessionField.NumberOfTables -> session.numberOfTables = field.parse(value) ?: 1 is SessionField.NumberOfTables -> session.numberOfTables = field.parse(value) ?: 1
is SessionField.Addon -> additionalBuyins += field.parse(value) ?: 0.0 is SessionField.Addon -> additionalBuyins += field.parse(value) ?: 0.0
is SessionField.Rebuy -> additionalBuyins += field.parse(value) ?: 0.0 is SessionField.Rebuy -> additionalBuyins += field.parse(value) ?: 0.0
is SessionField.Tips -> session.tips = field.parse(value) is SessionField.Tips -> session.result?.tips = field.parse(value)
is SessionField.HandsCount -> session.handsCount = field.parse(value) is SessionField.HandsCount -> session.handsCount = field.parse(value)
is SessionField.Break -> { is SessionField.Break -> {
field.parse(value)?.let { field.parse(value)?.let {
@ -191,7 +191,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
session.cgAnte = field.parse(value) session.cgAnte = field.parse(value)
} }
is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value) is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value)
is SessionField.TournamentPosition -> session.tournamentFinalPosition = is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition =
field.parse(value) field.parse(value)
is SessionField.TournamentName -> { is SessionField.TournamentName -> {
if (value.isNotEmpty()) { if (value.isNotEmpty()) {
@ -232,7 +232,6 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
Timber.d("N>> create: $number") Timber.d("N>> create: $number")
val entry = realm.copyToRealm(CustomFieldEntry()) val entry = realm.copyToRealm(CustomFieldEntry())
entry.numericValue = number entry.numericValue = number
entry.customField = customField
customField.entries.add(entry) customField.entries.add(entry)
session.customFieldEntries.add(entry) session.customFieldEntries.add(entry)
@ -256,12 +255,12 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate) val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate)
session.bankroll = bankroll session.bankroll = bankroll
session.buyin?.let { session.result?.buyin?.let {
session.buyin = it + additionalBuyins session.result?.buyin = it + additionalBuyins
} }
val net = session.net val net = session.result?.net
if (startDate != null) { // valid session if (startDate != null && net != null) { // valid session
// session already in realm, we'd love not put it in Realm before doing the check // session already in realm, we'd love not put it in Realm before doing the check
val count = DataUtils.sessionCount(realm, startDate!!, endDate, net) val count = DataUtils.sessionCount(realm, startDate!!, endDate, net)
if (this.noSessionImport || count == 0) { if (this.noSessionImport || count == 0) {

@ -46,10 +46,10 @@ class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg el
is SessionField.SessionType -> Session.Type.values()[data.type].value is SessionField.SessionType -> Session.Type.values()[data.type].value
is SessionField.Live -> field.format(data.isLive) is SessionField.Live -> field.format(data.isLive)
is SessionField.NumberOfTables -> field.format(data.numberOfTables) is SessionField.NumberOfTables -> field.format(data.numberOfTables)
is SessionField.Buyin -> field.format(data.buyin) is SessionField.Buyin -> field.format(data.result?.buyin)
is SessionField.CashedOut -> field.format(data.cashout) is SessionField.CashedOut -> field.format(data.result?.cashout)
is SessionField.NetResult -> field.format(data.netResult) is SessionField.NetResult -> field.format(data.result?.netResult)
is SessionField.Tips -> field.format(data.tips) is SessionField.Tips -> field.format(data.result?.tips)
is SessionField.HandsCount -> field.format(data.handsCount) is SessionField.HandsCount -> field.format(data.handsCount)
is SessionField.LimitType -> { is SessionField.LimitType -> {
data.limit?.let { limit -> data.limit?.let { limit ->
@ -76,7 +76,7 @@ class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg el
is SessionField.TournamentFeatures -> field.format(data.tournamentFeatures) is SessionField.TournamentFeatures -> field.format(data.tournamentFeatures)
is SessionField.TournamentEntryFee -> field.format(data.tournamentEntryFee) is SessionField.TournamentEntryFee -> field.format(data.tournamentEntryFee)
is SessionField.TournamentNumberOfPlayers -> field.format(data.tournamentNumberOfPlayers) is SessionField.TournamentNumberOfPlayers -> field.format(data.tournamentNumberOfPlayers)
is SessionField.TournamentPosition -> field.format(data.tournamentFinalPosition) is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition)
is SessionField.Comment -> data.comment is SessionField.Comment -> data.comment
is SessionField.NumberCustomField -> { is SessionField.NumberCustomField -> {
val entry = data.customFieldEntries.find { it.customField?.id == field.customField.id } val entry = data.customFieldEntries.find { it.customField?.id == field.customField.id }

@ -11,13 +11,7 @@ import org.apache.commons.csv.CSVRecord
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
/** enum class DataType {
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionTransactionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) :
PACSVDescriptor<Identifiable>(source, isTournament, *elements) {
private enum class DataType {
TRANSACTION, TRANSACTION,
SESSION; SESSION;
@ -32,8 +26,23 @@ class SessionTransactionCSVDescriptor(source: DataSource, isTournament: Boolean?
} }
} }
val workId: String
get() {
return when (this) {
TRANSACTION -> "transaction.work"
SESSION -> "session.work"
}
}
} }
/**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionTransactionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) :
PACSVDescriptor<Identifiable>(source, isTournament, *elements) {
/** /**
* Parses a [record] and return an optional Session * Parses a [record] and return an optional Session
*/ */

@ -7,8 +7,6 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.UsageCountable import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import timber.log.Timber
import java.util.*
fun <T : RealmModel>Realm.count(clazz: Class<T>) : Long { fun <T : RealmModel>Realm.count(clazz: Class<T>) : Long {
return this.where(clazz).count() return this.where(clazz).count()
@ -93,52 +91,13 @@ inline fun <reified C : RealmModel> Realm.sorted(editableOnly: Boolean = false,
return this.sorted(C::class.java, editableOnly, omitId = omitId) return this.sorted(C::class.java, editableOnly, omitId = omitId)
} }
fun Realm.writeAsync(handler: (Realm) -> (Unit)) {
Timber.d("Start write...")
this.executeTransactionAsync { asyncRealm ->
handler(asyncRealm)
}
// this.executeTransactionAsync({ asyncRealm ->
// handler(asyncRealm)
// }, { // success
//
// }, { error -> // error
// Timber.w("Realm write error: $error")
// })
}
fun Realm.writeAsync(handler: (Realm) -> (Unit), success: () -> (Unit)) {
Timber.d("Start write with success/error...")
val s = Date()
this.executeTransactionAsync({ asyncRealm ->
handler(asyncRealm)
Timber.d("REALM execution ended")
}, { // success
val e = Date()
val duration = e.time - s.time
Timber.d("//// transaction duration = $duration")
Timber.d("SUCCESS!")
success()
}, { error -> // error
Timber.d("Realm write error: $error")
})
}
/** /**
* Updates the useCount variable of the CountableUsage entity * Updates the useCount variable of the CountableUsage entity
*/ */
fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) { fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) {
this.writeAsync { asyncRealm -> val results = this.where(clazz).findAll()
val results = asyncRealm.where(clazz).findAll() this.executeTransaction {
results.forEach { countableUsage -> results.forEach { countableUsage ->
val countable = (countableUsage as UsageCountable) val countable = (countableUsage as UsageCountable)
@ -147,7 +106,7 @@ fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) {
TournamentFeature::class -> "tournamentFeatures.id" TournamentFeature::class -> "tournamentFeatures.id"
else -> "${clazz.simpleName.decapitalize()}.id" else -> "${clazz.simpleName.decapitalize()}.id"
} }
val count = asyncRealm.where(countable.ownerClass).contains(fieldName, countable.id).count().toInt() val count = it.where(countable.ownerClass).contains(fieldName, countable.id).count().toInt()
countable.useCount = count countable.useCount = count
} }
} }

@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView" android:id="@+id/nestedScrollView"

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true"
>
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"

@ -25,6 +25,7 @@
android:id="@+id/newTransaction" android:id="@+id/newTransaction"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginBottom="8dp"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
@ -102,6 +103,7 @@
android:id="@+id/newCashGame" android:id="@+id/newCashGame"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"

@ -47,6 +47,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/colorPrimary" android:background="@color/colorPrimary"
android:fitsSystemWindows="true"
android:padding="8dp" android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

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

Loading…
Cancel
Save