Compare commits

..

6 Commits

  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. 201
      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. 19
      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. 4
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  18. 5
      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. 26
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  29. 481
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  30. 18
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  31. 49
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  32. 61
      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. 39
      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. 65
      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. 49
      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. 27
      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. 23
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FiltersFragment.kt
  69. 22
      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. 99
      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. 351
      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. 8
      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. 111
      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. 6
      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. 85
      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. 41
      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

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

@ -1,6 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
// Crashlytics
@ -17,7 +17,7 @@ repositories {
android {
compileSdkVersion 35
compileSdkVersion 33
buildToolsVersion "30.0.3"
compileOptions {
@ -29,13 +29,16 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
lintOptions {
disable 'MissingTranslation'
}
defaultConfig {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 35
versionCode 180
versionName "6.0.38"
targetSdkVersion 33
versionCode 161
versionName "6.0.18"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -88,10 +91,6 @@ android {
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
@ -112,16 +111,13 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
// Places
implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0'
implementation 'com.android.billingclient:billing:5.0.0'
// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -143,10 +139,10 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera
def camerax_version = "1.1.0"
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
@ -162,17 +158,14 @@ dependencies {
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
// Test
testImplementation 'junit:junit:4.13.2'
testImplementation 'junit:junit:4.12'
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,7 @@
package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.DynamicRealmObject
import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber
@ -335,6 +336,47 @@ class PokerAnalyticsMigration : RealmMigration {
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 {

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

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

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

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

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

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

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

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

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

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

@ -2,15 +2,18 @@ package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
import java.util.*
open class UserConfig : RealmObject() {
companion object {
// these values are buffered to avoid the use of Realm instance
fun getConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config
@ -29,8 +32,12 @@ open class UserConfig : RealmObject() {
var transactionTypeIds: String = ""
fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) {
this.transactionTypeIds = transactionTypes.joinToString(UUID_SEPARATOR) { it.id }
// fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) {
// 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> {
@ -38,4 +45,40 @@ open class UserConfig : RealmObject() {
return ids.mapNotNull { realm.findById(it) }
}
}
}
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,18 +190,24 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/***
* Configures a hand history with a [handSetup]
*/
fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
fun configure(handSetup: HandSetup) {
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups)
}
this.playerSetups.removeAll(this.playerSetups)
handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it }
handSetup.blinds?.let { this.blinds = it }
this.session = handSetup.session
this.date = this.session?.handHistoryAutomaticDate ?: Date()
handSetup.session?.let { session ->
this.date = session.handHistoryAutomaticDate
session.realm?.let { realm ->
this.session = realm.copyFromRealm(session)
} ?: run {
this.session = session
}
} ?: run {
this.date = Date()
}
this.createActions(handSetup)
}
@ -315,13 +321,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
* Creates and affect a PlayerSetup at the given [positionIndex]
*/
fun createPlayerSetup(positionIndex: Int): PlayerSetup {
val playerSetup = if (this.realm != null) {
this.realm.createObject(PlayerSetup::class.java) }
else {
PlayerSetup()
}
val playerSetup = PlayerSetup()
playerSetup.position = positionIndex
this.playerSetups.add(playerSetup)
return playerSetup
@ -479,11 +479,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean?
get() {
return this.heroIndex?.let { heroIndex ->
this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
} ?: run {
null
@ -646,22 +642,19 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pots.forEach { pot ->
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
}

@ -16,7 +16,7 @@ class DataUtils {
fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int {
var sessionQuery = realm.where(Session::class.java)
.equalTo("startDate", startDate)
.equalTo("result.net", net)
.equalTo("net", net)
endDate?.let {
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
* Not suitable for display
*/
private fun Session.parameterRepresentation(context: Context): String {
private fun Session.parameterRepresentation(context: Context, realm: Realm): String {
var representation = ""
this.significantFields().forEach {
representation += this.charSequenceForRow(it, context)
representation += this.charSequenceForRow(it, context, realm)
}
return representation
@ -73,15 +73,20 @@ class FavoriteSessionFinder {
/**
* Copies the favorite session parameters on the [session]
*/
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) {
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context, realm: Realm) {
val favoriteSession = favoriteSession(session.type, location, session.realm, context)
val favoriteSession = favoriteSession(session.type, location, realm, context)
favoriteSession?.let { fav ->
session.limit = fav.limit
session.game = fav.game
session.bankroll = fav.bankroll
fav.game?.let { game ->
session.game = realm.copyFromRealm(game)
}
fav.bankroll?.let { br ->
session.bankroll = realm.copyFromRealm(br)
}
session.tableSize = fav.tableSize
when (session.type) {
@ -112,7 +117,7 @@ class FavoriteSessionFinder {
val counters = hashMapOf<String, Counter>()
lastSessions.forEach { session ->
val representation = session.parameterRepresentation(context)
val representation = session.parameterRepresentation(context, realm)
val counter = counters[representation]
if (counter != null) {
counter.increment()

@ -1,209 +0,0 @@
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,21 +6,18 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.databinding.ActivityHomeBinding
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.BackupTask
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.findAll
import net.pokeranalytics.android.util.extensions.isSameMonth
import java.util.*
@ -43,13 +40,12 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
}
}
private lateinit var currencies: RealmResults<Currency>
private var homePagerAdapter: HomePagerAdapter? = null
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (binding.viewPager.currentItem == Tab.REPORTS.identifier) {
this.paApplication.reportWhistleBlower?.clearNotifications()
DataManager.reportWhistleBlower?.clearNotifications()
}
when (item.itemId) {
@ -78,7 +74,6 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed()
lookForCalendarBadge()
checkForFailedBackups()
}
private lateinit var binding: ActivityHomeBinding
@ -98,32 +93,16 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
}
}
observeRealmObjects()
initUI()
checkFirstLaunch()
this.paApplication.reportWhistleBlower?.addListener(this)
DataManager.reportWhistleBlower?.addListener(this)
}
override fun onDestroy() {
super.onDestroy()
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()
}
}
}
DataManager.reportWhistleBlower?.removeListener(this)
}
/**
@ -166,12 +145,10 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
}
override fun newBestPerformanceHandler() {
if (Preferences.showInAppBadges(this)) {
binding.navigation.getOrCreateBadge(R.id.navigation_reports).isVisible = true
binding.navigation.getOrCreateBadge(R.id.navigation_reports).number = 1
}
}
private fun lookForCalendarBadge() {
@ -208,22 +185,4 @@ 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,14 +9,13 @@ import android.provider.MediaStore
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.util.ImageUtils
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.*
open class MediaActivity : BaseActivity() {
@ -52,12 +51,12 @@ open class MediaActivity : BaseActivity() {
val filesList = ArrayList<File>()
GlobalScope.launch {
CoroutineScope(context = Dispatchers.IO).launch {
if (tempFile != null) {
tempFile?.let {
GlobalScope.launch(Dispatchers.Main) {
filesList.add(it)
CoroutineScope(context = Dispatchers.Main).launch {
filesList.add(it)
getPictures(filesList)
}
}
@ -65,7 +64,7 @@ open class MediaActivity : BaseActivity() {
data.clipData?.let { clipData ->
try {
GlobalScope.launch(Dispatchers.Main) {
CoroutineScope(context = Dispatchers.Main).launch {
isLoadingNewPictures()
}
@ -78,7 +77,7 @@ open class MediaActivity : BaseActivity() {
filesList.add(photoFile)
}
GlobalScope.launch(Dispatchers.Main) {
CoroutineScope(context = Dispatchers.Main).launch {
getPictures(filesList)
}
@ -90,7 +89,7 @@ open class MediaActivity : BaseActivity() {
data.data?.let { uri ->
try {
GlobalScope.launch(Dispatchers.Main) {
CoroutineScope(context = Dispatchers.Main).launch {
isLoadingNewPictures()
}
@ -98,7 +97,7 @@ open class MediaActivity : BaseActivity() {
val photoFile = ImageUtils.createTempImageFile(this@MediaActivity)
ImageUtils.copyInputStreamToFile(inputStream!!, photoFile)
filesList.add(photoFile)
GlobalScope.launch(Dispatchers.Main) {
CoroutineScope(context = Dispatchers.Main).launch {
getPictures(filesList)
}
@ -176,13 +175,6 @@ 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
// }
}
@ -206,21 +198,6 @@ 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,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart
@ -103,7 +101,6 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
initData()
initUI()
loadGraph()
}
private fun initData() {

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

@ -1,8 +1,6 @@
package net.pokeranalytics.android.ui.fragment
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
@ -10,7 +8,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import io.realm.RealmResults
@ -21,8 +18,6 @@ import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -31,7 +26,7 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.calculus.DataManager
import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -47,7 +42,7 @@ import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences
import timber.log.Timber
import java.util.Date
import java.util.*
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
@ -188,38 +183,24 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
adapter = dataListAdapter
}
binding.addButton.setOnClickListener {
binding.addButton.setOnClickListener {
ReportCreationActivity.newInstanceForResult(this, requireContext())
}
val sessionCount = getRealm().where(Result::class.java).count()
binding.computeButton.isVisible = adapterRows.isEmpty() && sessionCount > 5
binding.computeButton.setOnClickListener {
try {
forceReportWhistleBlowerStart()
} catch (e: Exception) {
e.message?.let {
this.showSnackBar(it)
}
}
}
this.paApplication?.reportWhistleBlower?.addListener(this)
DataManager.reportWhistleBlower?.addListener(this)
}
override fun onDestroy() {
super.onDestroy()
this.paApplication?.reportWhistleBlower?.removeListener(this)
DataManager.reportWhistleBlower?.removeListener(this)
}
// Rows
private fun updateRows() {
this.adapterRows.clear()
if (this.reportSetups.isNotEmpty()) {
if (this.reportSetups.size > 0) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups)
}
@ -285,7 +266,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
override fun boolForRow(row: RowRepresentable): Boolean {
val reportRow = row as PerformanceRow
return Preferences.showInAppBadges(requireContext())
&& (this.paApplication?.reportWhistleBlower?.has(reportRow.performance.id) ?: false)
&& (DataManager.reportWhistleBlower?.has(reportRow.performance.id) ?: false)
}
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
@ -341,7 +322,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
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) {
if (!isDetached) {
@ -354,7 +335,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
}
override fun newBestPerformanceHandler() {
Timber.d("newBestPerformanceHandler called")
// Timber.d("newBestPerformanceHandler called")
activity?.runOnUiThread {
this.dataListAdapter.notifyDataSetChanged()
@ -362,32 +343,4 @@ 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,7 +16,6 @@ import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm
import net.pokeranalytics.android.BuildConfig
@ -45,21 +44,17 @@ import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.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.*
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.csv.ProductCSVDescriptors
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import net.pokeranalytics.android.util.extensions.writeAsync
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.Date
import java.util.*
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -193,17 +188,17 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
private fun updateMainCurrency(currencyCode: String, rate: Double) {
Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(Currency::class.java).findAll().forEach { currency ->
getRealm().writeAsync { asyncRealm ->
asyncRealm.where(Currency::class.java).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate
}
realm.where(Session::class.java).findAll().forEach { session ->
asyncRealm.where(Session::class.java).findAll().forEach { session ->
session.bankrollHasBeenUpdated()
}
}
realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
}
@ -324,8 +319,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed
}
} else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result.
}
}

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

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

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

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

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

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

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

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

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

@ -5,12 +5,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
@ -32,7 +30,6 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.StatRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import timber.log.Timber
import java.util.*
open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, CoroutineScope,
@ -84,11 +81,6 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
initData()
initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
report?.let {
showResults()
}
@ -210,10 +202,10 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
showLoader()
GlobalScope.launch(coroutineContext) {
CoroutineScope(context = Dispatchers.IO).launch(coroutineContext) {
var report: Report? = null
val test = GlobalScope.async {
val test = CoroutineScope(context = Dispatchers.IO).async {
val s = Date()
// Timber.d(">>> start...")
@ -228,7 +220,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
// Timber.d(">>> ended in $duration seconds")
}
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.ChipGroup
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.AggregationType
@ -30,7 +30,6 @@ import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.helpers.AppReviewManager
import timber.log.Timber
import java.util.*
@ -163,7 +162,7 @@ class ProgressReportFragment : AbstractReportFragment() {
graphContainer.hideWithAnimation()
progressBar.showWithAnimation()
GlobalScope.launch {
CoroutineScope(context = Dispatchers.IO).launch {
val s = Date()
// Timber.d(">>> start...")
@ -178,7 +177,7 @@ class ProgressReportFragment : AbstractReportFragment() {
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
// Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)

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

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

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

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

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

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

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

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

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

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

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

@ -20,9 +20,7 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.SessionRowView
import net.pokeranalytics.android.util.extensions.getMonthAndYear
import timber.log.Timber
import java.util.*
import kotlin.collections.HashMap
/**
@ -182,7 +180,7 @@ class FeedSessionRowRepresentableAdapter(
allSessions.clear()
allSessions.addAll(this.pendingSessions)
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?>()
@ -210,7 +208,7 @@ class FeedSessionRowRepresentableAdapter(
}
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.view.View
import android.view.ViewAnimationUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.pokeranalytics.android.databinding.ActivityNewDataBinding
@ -105,7 +105,8 @@ class NewDataMenuActivity : BaseActivity() {
val intent = Intent()
intent.putExtra(IntentKey.CHOICE.keyName, choice)
setResult(RESULT_OK, intent)
GlobalScope.launch(Dispatchers.Main) {
CoroutineScope(context = Dispatchers.Main).launch {
delay(200)
hideMenu()
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -3,6 +3,7 @@ package net.pokeranalytics.android.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorException
@ -211,17 +212,32 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
}
return null
}
BottomSheetType.MULTI_SELECTION -> this.selectedRows
BottomSheetType.MULTI_SELECTION -> this.selectedRowsRealmCopy()
BottomSheetType.NUMERIC_TEXT -> this.doubleValue
BottomSheetType.GRID -> this.defaultSize
BottomSheetType.DOUBLE_LIST, BottomSheetType.LIST_GAME -> this.someValues
BottomSheetType.LIST_STATIC -> this.selectedRows.firstOrNull()
BottomSheetType.LIST_STATIC -> this.selectedRowsRealmCopy().firstOrNull()
BottomSheetType.SUM -> this.doubleValue
BottomSheetType.CASH_GAME_STAKES -> Stakes(this.secondStringValue, this.ante)
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 {
return this.selectedRows.contains(row)
}
@ -243,9 +259,17 @@ class BottomSheetViewModel(var row: RowRepresentable) : ViewModel() {
}
fun rowSelected(position: Int): RowRepresentable? {
return when(this.row.bottomSheetType) {
BottomSheetType.LIST -> this.realmData?.get(position)
BottomSheetType.LIST_STATIC -> this.staticRows[position]
when(this.row.bottomSheetType) {
BottomSheetType.LIST -> {
this.realmData?.realm?.let { realm ->
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")
}
}

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

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

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

@ -33,7 +33,7 @@ class Preferences {
PATCH_SESSION_SETS("patchSessionSet"),
PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"),
// PATCH_BLINDS_FORMAT("patchBlindFormat"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions_v2"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions"),
PATCH_ZERO_TABLE("patchZeroTable"),
SHOW_STOP_NOTIFICATIONS("showStopNotifications"),
ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"),
@ -51,9 +51,7 @@ class Preferences {
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"),
PATCH_RATED_AMOUNT("patchRatedAmount[new field]"),
BACKUP_EMAIL("backupEmail"),
LANGUAGE_CODE("languageCode"),
SESSIONS_BACKUP_SUCCESS("sessionsBackupSuccess"),
TRANSACTIONS_BACKUP_SUCCESS("transactionsBackupSuccess")
LANGUAGE_CODE("languageCode")
}
enum class FeedMessage {
@ -354,21 +352,6 @@ class Preferences {
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,6 +7,7 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.ObjectIdentifier
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.writeAsync
import org.apache.commons.csv.CSVRecord
/**
@ -70,8 +71,8 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
if (realm.isInTransaction) {
this.deleteInsertedFromRealm(realm)
} else {
realm.executeTransaction {
this.deleteInsertedFromRealm(realm)
realm.writeAsync { asyncRealm ->
this.deleteInsertedFromRealm(asyncRealm)
}
}

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

@ -11,37 +11,28 @@ import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.util.*
enum class DataType {
TRANSACTION,
SESSION;
companion object {
fun valueForString(type: String): DataType? {
return when (type) {
"Deposit/Payout" -> TRANSACTION
"Cash Game", "Tournament" -> SESSION
else -> null
}
}
}
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) {
private enum class DataType {
TRANSACTION,
SESSION;
companion object {
fun valueForString(type: String): DataType? {
return when (type) {
"Deposit/Payout" -> TRANSACTION
"Cash Game", "Tournament" -> SESSION
else -> null
}
}
}
}
/**
* Parses a [record] and return an optional Session

@ -7,6 +7,8 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.realm.*
import timber.log.Timber
import java.util.*
fun <T : RealmModel>Realm.count(clazz: Class<T>) : Long {
return this.where(clazz).count()
@ -91,13 +93,52 @@ inline fun <reified C : RealmModel> Realm.sorted(editableOnly: Boolean = false,
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
*/
fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) {
val results = this.where(clazz).findAll()
this.executeTransaction {
this.writeAsync { asyncRealm ->
val results = asyncRealm.where(clazz).findAll()
results.forEach { countableUsage ->
val countable = (countableUsage as UsageCountable)
@ -106,7 +147,7 @@ fun <T : RealmModel>Realm.updateUsageCount(clazz: Class<T>) {
TournamentFeature::class -> "tournamentFeatures.id"
else -> "${clazz.simpleName.decapitalize()}.id"
}
val count = it.where(countable.ownerClass).contains(fieldName, countable.id).count().toInt()
val count = asyncRealm.where(countable.ownerClass).contains(fieldName, countable.id).count().toInt()
countable.useCount = count
}
}

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

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

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

@ -47,7 +47,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:fitsSystemWindows="true"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="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