Compare commits

...

94 Commits

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

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

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

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

@ -186,13 +186,13 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
Assert.fail("No std100 stat")
}
results.computedStat(Stat.MAXIMUM_NETRESULT)?.let {
results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let {
assertEquals(300.0, it.value, delta)
} ?: run {
Assert.fail("No MAXIMUM_NETRESULT")
}
results.computedStat(Stat.MINIMUM_NETRESULT)?.let {
results.computedStat(Stat.MINIMUM_NET_RESULT)?.let {
assertEquals(-100.0, it.value, delta)
} ?: run {
Assert.fail("No MINIMUM_NETRESULT")

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

@ -1,15 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.pokeranalytics.android">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<application
android:name=".PokerAnalyticsApplication"
@ -59,135 +62,186 @@
</activity>
<!-- DatabaseCopyActivity is only used in development for now -->
<!-- <activity android:name=".ui.activity.DatabaseCopyActivity"-->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <data android:scheme="content" />-->
<!-- <data android:scheme="file" />-->
<!-- <data android:mimeType="*/*" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"/>
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop"/>
android:launchMode="singleTop"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<service android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService" android:exported="false"/>
<service
android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<meta-data
android:name="preloaded_fonts"

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

@ -7,17 +7,15 @@ import com.google.firebase.FirebaseApp
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber
import java.util.*
@ -26,6 +24,7 @@ import java.util.*
class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object {
@ -83,10 +82,14 @@ class PokerAnalyticsApplication : Application() {
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// Infos
val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
@ -103,7 +106,7 @@ class PokerAnalyticsApplication : Application() {
realm.close()
if (sessionsCount < 10) {
GlobalScope.launch {
CoroutineScope(context = Dispatchers.IO).launch {
FakeDataManager.createFakeSessions(500)
}
}

@ -0,0 +1,75 @@
package net.pokeranalytics.android.api
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import timber.log.Timber
object RetrofitClient {
private const val BASE_URL = "https://www.pokeranalytics.net/backup/"
fun getClient(): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
class BackupService {
private val retrofit = RetrofitClient.getClient()
val backupApi: MyBackupApi = retrofit.create(MyBackupApi::class.java)
}
interface MyBackupApi {
@Multipart
@POST("send")
fun postFile(@Part mail: MultipartBody.Part, @Part fileBody: MultipartBody.Part): Call<Void>
}
object BackupApi {
private val service = BackupService()
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send
suspend fun backupFile(context: Context, mail: String, fileName: String, fileContent: String): Boolean {
val filePart = MultipartBody.Part.createFormData(
"file",
fileName,
RequestBody.create(MediaType.parse("text/csv"), fileContent)
)
val mailPart = MultipartBody.Part.createFormData("recipient", mail)
return if (context.isNetworkAvailable()) {
var success = false
val job = CoroutineScope(context = Dispatchers.IO).async {
success = try {
val response = service.backupApi.postFile(mailPart, filePart).execute()
Timber.d("response code = ${response.code()}")
Timber.d("success = ${response.isSuccessful}")
true
} catch (e: Exception) {
Timber.d("!!! backup failed: ${e.message}")
CrashLogging.logException(e)
false
}
}
job.await()
return success
} else {
false
}
}
}

@ -1,6 +1,7 @@
package net.pokeranalytics.android.api
import android.content.Context
import androidx.annotation.Keep
import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
@ -9,8 +10,11 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
@Keep
@Serializable
data class RateResponse(var info: RateInfo)
@Keep
@Serializable
data class RateInfo(var rate: Double)
@ -23,13 +27,8 @@ class CurrencyConverterApi {
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
// val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=9b56e742a75392c8aeb7"
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 }
Timber.d("Api call = $url")
val stringRequest = object : StringRequest(

@ -0,0 +1,242 @@
package net.pokeranalytics.android.api
import com.android.volley.*
import com.android.volley.toolbox.HttpHeaderParser
import java.io.*
open class VolleyMultipartRequest : Request<NetworkResponse?> {
private val twoHyphens = "--"
private val lineEnd = "\r\n"
private val boundary = "apiclient-" + System.currentTimeMillis()
private var mListener: Response.Listener<NetworkResponse>
private var mErrorListener: Response.ErrorListener
private var mHeaders: Map<String, String>? = null
private var byteData: Map<String, DataPart>? = null
/**
* Default constructor with predefined header and post method.
*
* @param url request destination
* @param headers predefined custom header
* @param listener on success achieved 200 code from request
* @param errorListener on error http or library timeout
*/
constructor(
url: String?, headers: Map<String, String>?,
byteData: Map<String, DataPart>,
listener: Response.Listener<NetworkResponse>,
errorListener: Response.ErrorListener
) : super(Method.POST, url, errorListener) {
mListener = listener
this.mErrorListener = errorListener
mHeaders = headers
this.byteData = byteData
}
/**
* Constructor with option method and default header configuration.
*
* @param method method for now accept POST and GET only
* @param url request destination
* @param listener on success event handler
* @param errorListener on error event handler
*/
constructor(
method: Int, url: String?,
listener: Response.Listener<NetworkResponse>,
errorListener: Response.ErrorListener
) : super(method, url, errorListener) {
mListener = listener
this.mErrorListener = errorListener
}
@Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
return if (mHeaders != null) mHeaders!! else super.getHeaders()
}
override fun getBodyContentType(): String {
return "multipart/form-data;boundary=$boundary"
}
@Throws(AuthFailureError::class)
override fun getBody(): ByteArray? {
val bos = ByteArrayOutputStream()
val dos = DataOutputStream(bos)
try {
// populate text payload
val params = params
if (params != null && params.isNotEmpty()) {
textParse(dos, params, paramsEncoding)
}
// populate data byte payload
val data =
byteData
if (data != null && data.isNotEmpty()) {
dataParse(dos, data)
}
// close multipart form data after text and file data
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
return bos.toByteArray()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse?> {
return try {
Response.success(
response,
HttpHeaderParser.parseCacheHeaders(response)
)
} catch (e: Exception) {
Response.error(ParseError(e))
}
}
override fun deliverResponse(response: NetworkResponse?) {
mListener.onResponse(response)
}
override fun deliverError(error: VolleyError?) {
mErrorListener.onErrorResponse(error)
}
/**
* Parse string map into data output stream by key and value.
*
* @param dataOutputStream data output stream handle string parsing
* @param params string inputs collection
* @param encoding encode the inputs, default UTF-8
* @throws IOException
*/
@Throws(IOException::class)
private fun textParse(
dataOutputStream: DataOutputStream,
params: Map<String, String>,
encoding: String
) {
try {
for ((key, value) in params) {
buildTextPart(dataOutputStream, key, value)
}
} catch (uee: UnsupportedEncodingException) {
throw RuntimeException("Encoding not supported: $encoding", uee)
}
}
/**
* Parse data into data output stream.
*
* @param dataOutputStream data output stream handle file attachment
* @param data loop through data
* @throws IOException
*/
@Throws(IOException::class)
private fun dataParse(dataOutputStream: DataOutputStream, data: Map<String, DataPart>) {
for ((key, value) in data) {
buildDataPart(dataOutputStream, value, key)
}
}
/**
* Write string data into header and data output stream.
*
* @param dataOutputStream data output stream handle string parsing
* @param parameterName name of input
* @param parameterValue value of input
* @throws IOException
*/
@Throws(IOException::class)
private fun buildTextPart(
dataOutputStream: DataOutputStream,
parameterName: String,
parameterValue: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"$parameterName\"$lineEnd")
//dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
dataOutputStream.writeBytes(lineEnd)
dataOutputStream.writeBytes(parameterValue + lineEnd)
}
/**
* Write data file into header and data output stream.
*
* @param dataOutputStream data output stream handle data parsing
* @param dataFile data byte as DataPart from collection
* @param inputName name of data input
* @throws IOException
*/
@Throws(IOException::class)
private fun buildDataPart(
dataOutputStream: DataOutputStream,
dataFile: DataPart,
inputName: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes(
"Content-Disposition: form-data; name=\"" +
inputName + "\"; filename=\"" + dataFile.fileName + "\"" + lineEnd
)
if (dataFile.type != null && !dataFile.type!!.trim { it <= ' ' }.isEmpty()) {
dataOutputStream.writeBytes("Content-Type: " + dataFile.type + lineEnd)
}
dataOutputStream.writeBytes(lineEnd)
val fileInputStream = ByteArrayInputStream(dataFile.content)
var bytesAvailable: Int = fileInputStream.available()
val maxBufferSize = 1024 * 1024
var bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffer = ByteArray(bufferSize)
var bytesRead: Int = fileInputStream.read(buffer, 0, bufferSize)
while (bytesRead > 0) {
dataOutputStream.write(buffer, 0, bufferSize)
bytesAvailable = fileInputStream.available()
bufferSize = Math.min(bytesAvailable, maxBufferSize)
bytesRead = fileInputStream.read(buffer, 0, bufferSize)
}
dataOutputStream.writeBytes(lineEnd)
}
/**
* Simple data container use for passing byte file
*/
class DataPart {
var fileName: String? = null
var content: ByteArray? = null
var type: String? = null
/**
* Constructor with data.
*
* @param name label of data
* @param data byte data
*/
constructor(name: String?, data: ByteArray) {
fileName = name
content = data
}
/**
* Constructor with mime data type.
*
* @param name label of data
* @param data byte data
* @param mimeType mime data like "image/jpeg"
*/
constructor(name: String?, data: ByteArray, mimeType: String?) {
fileName = name
content = data
type = mimeType
}
}
}

@ -382,6 +382,7 @@ class Calculator {
results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session)
results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session)
results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
results.addEvolutionValue(tBBSum, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO,

@ -12,6 +12,7 @@ import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurati
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
@ -32,7 +33,7 @@ class ReportWhistleBlower(var context: Context) {
private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
private var paused: Boolean = false
var paused: Boolean = false
private var timer: CountDownTimer? = null
@ -62,9 +63,10 @@ class ReportWhistleBlower(var context: Context) {
}
fun requestReportLaunch() {
Timber.d(">>> Launch report")
// Timber.d(">>> Launch report")
if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return
}
@ -131,10 +133,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private var cancelled = false
var handler: (() -> Unit)? = null
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() {
messages.add("Starting task...")
launchReports()
}
@ -142,6 +147,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
this.cancelled = true
}
var messages: MutableList<String> = mutableListOf()
private fun launchReports() {
CoroutineScope(coroutineContext).launch {
@ -171,7 +178,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private fun launchReport(realm: Realm, report: StaticReport) {
Timber.d(">>> launch report = $report")
// Timber.d(">>> launch report = $report")
when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, report)
@ -190,19 +197,23 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.values().forEach { key ->
LiveOnline.entries.forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive)
analyseOptimalDuration(realm, report, key, duration)
}
this.handler?.let { it() }
}
private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) {
messages.add("Analyse report $staticReport...")
val nameSeparator = " "
for (stat in result.options.stats) {
Timber.d("analyse stat: $stat for report: $staticReport")
// Timber.d("analyse stat: $stat for report: $staticReport")
// Get current performance
var query = performancesQuery(realm, staticReport, stat)
@ -217,6 +228,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
// Store if necessary, delete if necessary
val bestComputedResults = result.max(stat)
bestComputedResults?.let { computedResults ->
messages.add("found new perf...")
val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator)
@ -225,6 +237,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
var storePerf = true
currentPerf?.let {
messages.add("has current perf...")
currentPerf.name?.let { name ->
if (computedResults.group.query.getName(this.context, nameSeparator) == name) {
storePerf = false
@ -246,6 +260,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
}
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) {
val performance = Performance(
@ -261,10 +276,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
} ?: run { // if there is no max but a now irrelevant Performance, we delete it
Timber.d("NO best computed value, current perf = $currentPerf ")
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}")
// Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
perf.deleteFromRealm()
}
}

@ -208,7 +208,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records
NET_RESULT -> R.string.total
NET_RESULT, BB_NET_RESULT -> R.string.total
STANDARD_DEVIATION -> R.string.net_result
STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses

@ -65,7 +65,7 @@ class CashGameOptimalDurationCalculator {
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
Timber.d("Stop notif > keys: $hkeys ")
// Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
@ -76,15 +76,15 @@ class CashGameOptimalDurationCalculator {
validBuckets++
}
}
Timber.d("Stop notif > validBuckets: $validBuckets ")
// Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
Timber.d("Stop notif > invalid setup: $start / $end ")
// Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
// Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
@ -134,7 +134,7 @@ class CashGameOptimalDurationCalculator {
return bestDuration
}
Timber.d("Stop notif > not found, best duration: $bestDuration")
// Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}

@ -2,6 +2,7 @@ package net.pokeranalytics.android.model.extensions
import android.content.Context
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import net.pokeranalytics.android.R
@ -119,7 +120,7 @@ fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
.addTag(this.id)
.build()
WorkManager.getInstance(context).enqueue(work)
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
}

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

@ -4,27 +4,20 @@ import android.content.Context
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.RANDOM_PLAYER
import net.pokeranalytics.android.util.extensions.isSameDay
import net.pokeranalytics.android.util.extensions.mediumDate
import java.util.*
import kotlin.collections.ArrayList
open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable {
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -44,13 +37,6 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
@Ignore
override val viewType: Int = RowViewType.ROW_PLAYER.ordinal
@Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf()
@Ignore
private var commentsToDelete: ArrayList<Comment> = ArrayList()
override fun isValidForDelete(realm: Realm): Boolean {
//TODO
return true
@ -69,70 +55,16 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
return R.string.relationship_error
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
PlayerPropertiesRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerPropertiesRow.IMAGE -> this.picture = value as String? ?: ""
}
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(PlayerPropertiesRow.IMAGE)
rows.add(PlayerPropertiesRow.NAME)
rows.add(PlayerPropertiesRow.SUMMARY)
if (comments.size > 0) {
// Adds Comments section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, R.string.comments))
val currentCommentCalendar = Calendar.getInstance()
val currentDateCalendar = Calendar.getInstance()
val commentsToDisplay = ArrayList<Comment>()
commentsToDisplay.addAll(comments)
commentsToDisplay.sortByDescending { it.date }
commentsToDisplay.forEachIndexed { index, comment ->
currentCommentCalendar.time = comment.date
if (!currentCommentCalendar.isSameDay(currentDateCalendar) || index == 0) {
currentDateCalendar.time = currentCommentCalendar.time
// Adds day sub section
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_SUBTITLE, title = currentDateCalendar.time.mediumDate()))
}
// Adds comment
rows.add(comment)
}
rows.add(SeparatorRow())
PlayerPropertiesRow.IMAGE -> this.picture = value as? String
}
return rows
}
/**
@ -142,57 +74,6 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
return picture != null && picture?.isNotEmpty() == true
}
/**
* Update row representation
*/
fun updateRowRepresentation() {
this.rowRepresentation = this.updatedRowRepresentationForCurrentState()
}
/**
* Add an entry
*/
fun addComment(): Comment {
val entry = Comment()
this.comments.add(entry)
updateRowRepresentation()
return entry
}
/**
* Delete an entry
*/
fun deleteComment(comment: Comment) {
commentsToDelete.add(comment)
this.comments.remove(comment)
updateRowRepresentation()
}
/**
* Clean up deleted entries
*/
fun cleanupComments() { // called when saving the custom field
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
this.commentsToDelete.forEach { // entries are out of realm
realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
}
}
realm.close()
this.commentsToDelete.clear()
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
when (row) {
PlayerPropertiesRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name))
PlayerPropertiesRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.summary))
}
return null
}
val initials: String
get() {
return if (this.name.isNotEmpty()) {
@ -213,4 +94,8 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
}
}
fun hands(realm: Realm): RealmResults<HandHistory> {
return realm.where(HandHistory::class.java).equalTo("playerSetups.player.id", this.id).findAll()
}
}

@ -105,7 +105,7 @@ 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
}

@ -676,42 +676,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
}
fun getFormattedStakes(): String {
return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT }
//
// val formattedBlinds = StakesHolder.formattedBlinds(this.cgBlinds, this.currency)
// val formattedAntes = StakesHolder.formattedAnte(this.cgAnte, this.currency)
//
// return StakesHolder.formattedStakes(formattedBlinds, formattedAntes)
//
//
// val components = arrayListOf<String>()
// this.formattedBlinds?.let { components.add(it) }
// this.formattedAnte?.let { components.add("($it)") }
//
// return if (components.isNotEmpty()) {
// components.joinToString(" ")
// } else {
// NULL_TEXT
// }
}
// fun formatBlinds() {
// blinds = null
// if (cgBigBlind == null) return
// cgBigBlind?.let { bb ->
// val sb = cgSmallBlind ?: bb / 2.0
// val preFormattedBlinds = "${sb.formatted}/${bb.round()}"
// println("<<<<<< bb.toCurrency(currency) : ${bb.toCurrency(currency)}")
// println("<<<<<< preFormattedBlinds : $preFormattedBlinds")
// val regex = Regex("-?\\d+(\\.\\d+)?")
// blinds = bb.toCurrency(currency).replace(regex, preFormattedBlinds)
// println("<<<<<< blinds = $blinds")
// }
// }
// LifeCycle
/**
@ -719,8 +686,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
*/
fun delete() {
CrashLogging.log("Deletes session. Id = ${this.id}")
if (isValid) {
// CrashLogging.log("Deletes session. Id = ${this.id}")
realm.executeTransaction {
cleanup()
deleteFromRealm()
@ -776,32 +743,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
when (row) {
SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
SessionPropertiesRow.STAKES -> if (value is Stakes) {
if (value.ante != null) {
this.cgAnte = value.ante
}
if (value.blinds != null) {
this.cgBlinds = value.blinds
}
// cgSmallBlind = try {
// (value[0] as String? ?: "0").toDouble()
// } catch (e: Exception) {
// null
// }
//
// cgBigBlind = try {
// (value[1] as String? ?: "0").toDouble()
// } catch (e: Exception) {
// null
// }
//
// cgBigBlind?.let {
// if (cgSmallBlind == null || cgSmallBlind == 0.0) {
// cgSmallBlind = it / 2.0
// }
// }
} else if (value == null) {
this.cgBlinds = null
this.cgAnte = null
@ -812,7 +759,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> {
val localResult = getOrCreateResult()
localResult.buyin = value as Double?
// this.updateRowRepresentation()
}
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
val localResult = getOrCreateResult()
@ -951,7 +897,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.bbNet,
this.estimatedHands
)
Stat.AVERAGE_NET_BB -> this.bbNet
Stat.AVERAGE_NET_BB, Stat.BB_NET_RESULT -> this.bbNet
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
Stat.HANDS_PLAYED -> this.estimatedHands
@ -1086,33 +1032,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.result?.netResult = null
}
/// Stakes
// fun generateStakes() {
//
// if (this.cgAnte == null && this.cgAnte == null) {
// this.cgStakes = null
// return
// }
//
// val components = arrayListOf<String>()
//
// this.cgBlinds?.let { components.add("${cbBlinds}${it}") }
// this.cgAnte?.let { components.add("${cbAnte}${it.formatted}") }
//
// val code = this.bankroll?.currency?.code ?: UserDefaults.currency.currencyCode
// components.add("${cbCode}${code}")
//
// this.cgStakes = components.joinToString(cbSeparator)
// }
//
// fun defineHighestBet() {
// val bets = arrayListOf<Double>()
// this.cgAnte?.let { bets.add(it) }
// bets.addAll(this.blindValues)
// this.cgBiggestBet = bets.maxOrNull()
// }
private fun cleanupBlinds(blinds: String?): String? {
if (blinds == null) {

@ -31,7 +31,8 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
BONUS(2, true),
STACKING_INCOMING(3, true),
STACKING_OUTGOING(4, false),
TRANSFER(5, false);
TRANSFER(5, false),
EXPENSE(6, false); // not created by default, only used for poker base import atm
companion object : IntSearchable<Value> {
@ -49,6 +50,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
STACKING_INCOMING -> R.string.stacking_incoming
STACKING_OUTGOING -> R.string.stacking_outgoing
TRANSFER -> R.string.transfer
EXPENSE -> R.string.expense
}
}
@ -70,6 +72,10 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
throw PAIllegalStateException("Transaction type ${value.name} should exist in database!")
}
fun getOrCreate(realm: Realm, value: Value, context: Context): TransactionType {
return getOrCreate(realm, value.localizedTitle(context), value.additive)
}
fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst()
return if (type != null) {

@ -190,9 +190,11 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/***
* Configures a hand history with a [handSetup]
*/
fun configure(handSetup: HandSetup) {
fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
this.playerSetups.removeAll(this.playerSetups)
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it }
@ -477,7 +479,12 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean?
get() {
return this.heroIndex?.let { heroIndex ->
this.winnerPots.any { it.position == heroIndex }
this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
} ?: run {
null
}
@ -639,19 +646,22 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pots.forEach { pot ->
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
}
}
@ -733,7 +743,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return boardHasWildCard || playerCardHasWildCard
}
val allFullCards: List<Card>
private val allFullCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
@ -753,4 +763,13 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return cards.filter { it.isSuitWildCard }
}
private val largestWonPot: WonPot?
get() {
return if (this.winnerPots.isNotEmpty()) { // needed, otherwise maxBy crashes
this.winnerPots.maxBy { it.amount }
} else {
null
}
}
}

@ -17,15 +17,19 @@ class Seed(var context:Context) : Realm.Transaction {
fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) {
values.forEach { value ->
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
if (value != TransactionType.Value.EXPENSE) {
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
}
}
}
}

@ -0,0 +1,99 @@
package net.pokeranalytics.android.ui.activity
import android.Manifest
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.activity.components.ResultCode
import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.util.copyStreamToFile
import timber.log.Timber
import java.io.File
class DatabaseCopyActivity : BaseActivity() {
private lateinit var fileURI: Uri
enum class IntentKey(val keyName: String) {
URI("uri")
}
companion object {
/**
* Create a new instance for result
*/
fun newInstanceForResult(context: FragmentActivity, uri: Uri) {
context.startActivityForResult(getIntent(context, uri), RequestCode.IMPORT.value)
}
private fun getIntent(context: Context, uri: Uri): Intent {
Timber.d("getIntent")
val intent = Intent(context, DatabaseCopyActivity::class.java)
intent.putExtra(IntentKey.URI.keyName, uri)
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("onCreate")
intent?.data?.let {
this.fileURI = it
} ?: run {
this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) ?: throw PAIllegalStateException("Uri not found")
}
// setContentView(R.layout.activity_import)
requestImportConfirmation()
}
private fun initUI() {
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), RequestCode.PERMISSION_WRITE_EXTERNAL_STORAGE.value) {
val path = Realm.getDefaultInstance().path
contentResolver.openInputStream(fileURI)?.let { inputStream ->
val destination = File(path)
inputStream.copyStreamToFile(destination)
toast("Please restart app")
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RequestCode.IMPORT.value -> {
if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) {
showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = {
finish()
})
}
}
}
}
// Import
private fun requestImportConfirmation() {
showAlertDialog(context = this, title = R.string.import_confirmation, showCancelButton = true, positiveAction = {
initUI()
}, negativeAction = {
finish()
})
}
}

@ -17,8 +17,10 @@ import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.BackupTask
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.findAll
import net.pokeranalytics.android.util.extensions.isSameMonth
import java.util.*
@ -76,6 +78,7 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed()
lookForCalendarBadge()
checkForFailedBackups()
}
private lateinit var binding: ActivityHomeBinding
@ -205,4 +208,22 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
}
private fun checkForFailedBackups() {
if (!Preferences.sessionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.SESSION, email, this)
task.start()
}
}
if (!Preferences.transactionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.TRANSACTION, email, this)
task.start()
}
}
}
}

@ -18,7 +18,6 @@ import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.fragment.ImportFragment
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.extensions.count
import timber.log.Timber
class ImportActivity : BaseActivity() {
@ -64,11 +63,13 @@ class ImportActivity : BaseActivity() {
val fragmentTransaction = supportFragmentManager.beginTransaction()
val fragment = ImportFragment()
val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis")
fis?.let {
fragment.setData(it)
}
fragment.setData(fileURI)
// val fis = contentResolver.openInputStream(fileURI)
// Timber.d("Load fragment data with: $fis")
// fis?.let {
// fragment.setData(it)
// }
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()

@ -3,6 +3,7 @@ package net.pokeranalytics.android.ui.activity.components
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.view.MenuItem
@ -21,6 +22,8 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.LocationManager
import net.pokeranalytics.android.util.PermissionRequest
import net.pokeranalytics.android.util.Preferences
import java.util.*
class RootBottomSheetViewModel: ViewModel() {
var rowRepresentable: RowRepresentable? = null
@ -55,12 +58,14 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState")
setLanguage()
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState")
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash
setLanguage()
}
override fun onResume() {
@ -129,6 +134,26 @@ abstract class BaseActivity : AppCompatActivity() {
fragmentTransaction.commit()
}
private fun setLanguage() {
Preferences.getLanguageCode(this)?.let { languageCode ->
val config = resources.configuration
// val lang = "de" // your language code
val locale = Locale(languageCode)
Locale.setDefault(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
config.setLocale(locale)
} else {
config.locale = locale
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
createConfigurationContext(config)
resources.updateConfiguration(config, resources.displayMetrics)
}
}
/**
* Return the realm instance

@ -0,0 +1,187 @@
package net.pokeranalytics.android.ui.activity.components
import android.Manifest
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.databinding.ActivityCameraBinding
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraActivity : BaseActivity() {
companion object {
const val IMAGE_URI = "image_uri"
fun newInstanceForResult(fragment: Fragment, code: RequestCode) {
val intent = Intent(fragment.requireContext(), CameraActivity::class.java)
fragment.startActivityForResult(intent, code.value)
}
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
private lateinit var viewBinding: ActivityCameraBinding
private lateinit var cameraExecutor: ExecutorService
private var imageCapture: ImageCapture? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.viewBinding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
if (allPermissionsGranted()) {
cameraExecutor = Executors.newSingleThreadExecutor()
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
override fun onDestroy() {
super.onDestroy()
this.cameraExecutor.shutdown()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(this.viewBinding.viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder().build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Timber.e(exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time stamped name and MediaStore entry.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(e: ImageCaptureException) {
Timber.e(e)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
// val msg = "Photo capture succeeded: ${output.savedUri}"
// Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
// Timber.d(msg)
val intent = Intent()
intent.putExtra(IMAGE_URI, output.savedUri.toString())
setResult(Activity.RESULT_OK, intent)
finish()
}
}
)
}
}

@ -16,7 +16,8 @@ enum class RequestCode(var value: Int) {
IMPORT(900),
SUBSCRIPTION(901),
CURRENCY(902),
PERMISSION_WRITE_EXTERNAL_STORAGE(1000)
PERMISSION_WRITE_EXTERNAL_STORAGE(1000),
CAMERA(1001)
}
enum class ResultCode(var value: Int) {

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

@ -163,14 +163,26 @@ fun showEditTextAlertDialog(
builder.setMessage(it)
}
val layout = LinearLayout(context)
layout.orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
)
params.setMargins(20, 0, 30, 0)
val editText = EditText(context)
editText.inputType = inputType
editTextText?.let {
editText.text = SpannableStringBuilder(it)
}
editText.setTextColor(ContextCompat.getColor(context, R.color.white))
editText.inputType = inputType
builder.setView(editText)
layout.addView(editText, params)
builder.setView(layout)
// builder.setView(editText)
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
positiveAction?.invoke(editText.text.toString())

@ -45,27 +45,46 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
)
}
private val availableCurrencies = this.systemCurrencies.filter {
!mostUsedCurrencyCodes.contains(it.currencyCode)
}.filter {
UserDefaults.availableCurrencyLocales.filter { currencyLocale ->
Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
}.isNotEmpty()
}.sortedBy {
it.displayName
}.map {
CurrencyRow(it)
}
private val availableCurrencies =
Locale.getAvailableLocales()
.mapNotNull {
try {
Currency.getInstance(it)
} catch (e: Exception) {
null
}
}.toSet()
.filter { !mostUsedCurrencyCodes.contains(it.currencyCode) }
.filter {
UserDefaults.availableCurrencyLocales.any { currencyLocale ->
Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
}
}
.sortedBy { it.displayName }
.map { CurrencyRow(it) }
// private val availableCurrencies = this.systemCurrencies.filter {
// !mostUsedCurrencyCodes.contains(it.currencyCode)
// }.filter {
// UserDefaults.availableCurrencyLocales.filter { currencyLocale ->
// Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
// }.isNotEmpty()
// }.sortedBy {
// it.displayName
// }.map {
// CurrencyRow(it)
// }
}
private class CurrencyRow(var currency: Currency) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return currency.getDisplayName(Locale.getDefault()).capitalize()
return this.currency.getDisplayName(Locale.getDefault()).capitalize()
}
var currencyCode: String = currency.currencyCode
var currencySymbol: String = currency.getSymbol(Locale.getDefault())
var currencyCode: String = this.currency.currencyCode
var currencySymbol: String = this.currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
@ -110,9 +129,6 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
// RowRepresentableDelegate
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
val intent = Intent()
intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode)
this.activity?.setResult(Activity.RESULT_OK, intent)

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

@ -1,5 +1,6 @@
package net.pokeranalytics.android.ui.fragment
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -15,6 +16,7 @@ import net.pokeranalytics.android.databinding.FragmentImportBinding
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate
import net.pokeranalytics.android.util.csv.ImportException
import timber.log.Timber
import java.io.InputStream
import java.text.NumberFormat
@ -24,6 +26,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private lateinit var filePath: String
private lateinit var inputStream: InputStream
private lateinit var uri: Uri
private lateinit var importer: CSVImporter
private var _binding: FragmentImportBinding? = null
@ -52,6 +55,10 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.inputStream = inputStream
}
fun setData(uri: Uri) {
this.uri = uri
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -86,7 +93,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.parentActivity?.paApplication?.reportWhistleBlower?.pause()
this.importer = CSVImporter(inputStream)
this.importer = CSVImporter(uri, requireContext())
this.importer.delegate = this
CoroutineScope(coroutineContext).launch {
@ -95,7 +102,11 @@ class ImportFragment : RealmFragment(), ImportDelegate {
val s = Date()
Timber.d(">>> Start Import...")
importer.start()
try {
importer.start()
} catch (e: ImportException) {
exceptions.add(e)
}
val e = Date()
val duration = (e.time - s.time) / 1000.0
@ -116,15 +127,6 @@ class ImportFragment : RealmFragment(), ImportDelegate {
snackBar.show()
}
// if (shouldDismissActivity) {
//
// activity?.let {
// it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value)
// it.finish()
// }
//
// } else {
// }
importDidFinish()
}

@ -1,6 +1,8 @@
package net.pokeranalytics.android.ui.fragment
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
@ -8,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import io.realm.RealmResults
@ -18,6 +21,8 @@ import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -26,6 +31,7 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -41,7 +47,7 @@ import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences
import timber.log.Timber
import java.util.*
import java.util.Date
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
@ -182,10 +188,24 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
adapter = dataListAdapter
}
binding.addButton.setOnClickListener {
binding.addButton.setOnClickListener {
ReportCreationActivity.newInstanceForResult(this, requireContext())
}
val sessionCount = getRealm().where(Result::class.java).count()
binding.computeButton.isVisible = adapterRows.isEmpty() && sessionCount > 5
binding.computeButton.setOnClickListener {
try {
forceReportWhistleBlowerStart()
} catch (e: Exception) {
e.message?.let {
this.showSnackBar(it)
}
}
}
this.paApplication?.reportWhistleBlower?.addListener(this)
}
@ -199,7 +219,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
private fun updateRows() {
this.adapterRows.clear()
if (this.reportSetups.size > 0) {
if (this.reportSetups.isNotEmpty()) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups)
}
@ -342,4 +362,32 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
}
private fun forceReportWhistleBlowerStart() {
val rwb = ReportWhistleBlower(requireContext())
val reportTask = ReportTask(rwb, requireContext())
reportTask.handler = {
Timber.d("test")
val paused = paApplication?.reportWhistleBlower?.paused
reportTask.messages.add(">>> main RWB paused = $paused")
val message = reportTask.messages.joinToString("\n")
CoroutineScope(coroutineContext).launch(Dispatchers.Main) {
Timber.d("test2")
copyToClipboard(requireContext(), message)
}
}
reportTask.start()
}
fun copyToClipboard(context: Context, text: String) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("label", text)
clipboard.setPrimaryClip(clip)
}
}

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

@ -162,7 +162,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val async = GlobalScope.async {
val s = Date()
Timber.d(">>> start...")
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()
@ -174,7 +174,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
// Timber.d(">>> ended in $duration seconds")
}
async.await()
@ -190,8 +190,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/
private fun createSessionGroupsAndStartCompute(realm: Realm): Report {
val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let {
if (it.filterableType == currentFilterable) { it } else { null }
var filter: Filter? = null
context?.let { context ->
this.currentFilter(context, realm)?.let { current ->
if (current.filterableType == currentFilterable) { filter = current }
}
}
val allStats: List<Stat> = listOf(
@ -238,7 +241,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats)
Timber.d(">>>>> Start computations...")
// Timber.d(">>>>> Start computations...")
val options = Calculator.Options()
val computedStats = mutableListOf<Stat>()

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

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

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

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

@ -4,6 +4,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmResults

@ -112,10 +112,10 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
private fun initModel() {
val row = config?.row
?: (requireActivity() as? BaseActivity)?.bottomSheetViewModel?.rowRepresentable
?: (activity as? BaseActivity)?.bottomSheetViewModel?.rowRepresentable
?: throw PAIllegalStateException("row not found")
val delegate = config?.delegate
?: (requireActivity() as? BaseActivity)?.bottomSheetViewModel?.delegate
?: (activity as? BaseActivity)?.bottomSheetViewModel?.delegate
?: throw PAIllegalStateException("delegate not found")
val factory = BottomSheetViewModelFactory(row, delegate)

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

@ -3,11 +3,9 @@ package net.pokeranalytics.android.ui.fragment.report
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
@ -60,8 +58,8 @@ abstract class AbstractReportFragment : DataManagerFragment() {
override fun saveData() {
activity?.let {
val builder = AlertDialog.Builder(it)
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
// Get the layout inflater
val inflater = requireActivity().layoutInflater
@ -94,19 +92,6 @@ abstract class AbstractReportFragment : DataManagerFragment() {
}
val dialog = builder.create()
dialog.setOnShowListener {
nameEditText.requestFocus()
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
dialog.setOnDismissListener {
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
dialog.show()
} ?: throw PAIllegalStateException("Activity cannot be null")

@ -5,6 +5,8 @@ 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
@ -12,11 +14,11 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentComposableTableReportBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.ReportActivity
@ -82,6 +84,11 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
initData()
initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
report?.let {
showResults()
}
@ -156,25 +163,17 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
private fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
val rows: ArrayList<RowRepresentable> = ArrayList()
report.results.forEach { result ->
val title = result.group.query.getName(requireContext()).capitalize()
rows.add(CustomizableRowRepresentable(title = title))
val statList = result.group.displayedStats ?: report.options.stats
statList.forEach { stat ->
rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(requireContext())))
this.context?.let { context ->
report.results.forEach { result ->
val title = result.group.query.getName(context).capitalize()
rows.add(CustomizableRowRepresentable(title = title))
val statList = result.group.displayedStats ?: report.options.stats
statList.forEach { stat ->
rows.add(StatRow(stat, result.computedStat(stat), result.group.query.getName(context)))
}
}
}
return rows
// val rows: ArrayList<RowRepresentable> = ArrayList()
// report.options.stats.forEach {stat ->
// rows.add(CustomizableRowRepresentable(title = stat.localizedTitle(requireContext())))
// report.results.forEach {
// val title = it.group.name
// rows.add(StatRow(stat, it.computedStat(stat), it.group.name, title))
// }
// }
// return rows
}
// RowRepresentableDelegate
@ -216,7 +215,7 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
var report: Report? = null
val test = GlobalScope.async {
val s = Date()
Timber.d(">>> start...")
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()

@ -15,11 +15,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.*
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.*
import net.pokeranalytics.android.databinding.FragmentProgressReportBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.combined
@ -82,12 +82,6 @@ class ProgressReportFragment : AbstractReportFragment() {
override fun onDestroyView() {
// childFragmentManager.findFragmentByTag(GRAPH_TAG)?.let { fragment ->
// val fragmentTransaction = childFragmentManager.beginTransaction()
// fragmentTransaction.remove(fragment)
// fragmentTransaction.commit()
// }
AppReviewManager.requestReview()
super.onDestroyView()
@ -172,26 +166,27 @@ class ProgressReportFragment : AbstractReportFragment() {
GlobalScope.launch {
val s = Date()
Timber.d(">>> start...")
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
val group = selectedReport.results.first().group
val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType)
reports[aggregationType] = report
selectedReport.results.firstOrNull()?.group?.let { group ->
val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, group, aggregationType)
reports[aggregationType] = report
realm.close()
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)
progressBar.hideWithAnimation()
graphContainer.showWithAnimation()
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)
progressBar.hideWithAnimation()
graphContainer.showWithAnimation()
}
}
}
}

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

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

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

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

@ -8,6 +8,7 @@ import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
@ -254,10 +255,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
binding.messageBox.isVisible = false
}
val viewManager = SmoothScrollLinearLayoutManager(requireContext())
// val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.menuRecyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
layoutManager = LinearLayoutManager(requireContext())
}
// Add button
@ -582,38 +583,38 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
private fun retrieveLatestBlogPosts() {
val now = Date().time
// if (true) {
if (Preferences.shouldShowBlogTips(requireContext()) && Preferences.getLastBlogTipsRetrievalDate(requireContext()) + 24 * 3600 * 1000 < now) {
this.context?.let { context ->
val now = Date().time
if (Preferences.shouldShowBlogTips(context) && Preferences.getLastBlogTipsRetrievalDate(context) + 24 * 3600 * 1000 < now) {
BlogPostApi.getLatestPosts(requireContext()) { posts ->
BlogPostApi.getLatestPosts(context) { posts ->
context?.let {
Preferences.setLastBlogTipsRetrievalDate(now, requireContext())
Preferences.setLastBlogTipsRetrievalDate(now, context)
var count = 0
if (posts.isNotEmpty()) {
Preferences.setLatestRetrievedBlogPostId(posts.first().id, requireContext())
val id = Preferences.getLatestDisplayedBlogPostId(requireContext())
Preferences.setLatestRetrievedBlogPostId(posts.first().id, context)
val id = Preferences.getLatestDisplayedBlogPostId(context)
count = posts.count { it.id > id }
}
displayBlogPostButton(count)
}
}
}
}
private fun displayBlogPostButton(newCount: Int = 0) {
var show = false
if (Preferences.shouldShowBlogTips(requireContext()) && newCount > 0) {
show = true
this.badgeDrawable?.number = newCount
context?.let { context ->
var show = false
if (Preferences.shouldShowBlogTips(context) && newCount > 0) {
show = true
this.badgeDrawable?.number = newCount
}
this.binding.postButton.isVisible = show
this.badgeDrawable?.isVisible = show
}
this.binding.postButton.isVisible = show
this.badgeDrawable?.isVisible = show
}
}

@ -117,14 +117,18 @@ class NewDataMenuActivity : BaseActivity() {
private fun showMenu() {
val menuContainer = binding.menuContainer
val cx = menuContainer.measuredWidth - fabSize / 2
val cy = menuContainer.measuredHeight - fabSize / 2
val finalRadius = max(menuContainer.width, menuContainer.height)
val anim = ViewAnimationUtils.createCircularReveal(menuContainer, cx, cy, 0f, finalRadius.toFloat())
anim.duration = 150
menuContainer.visibility = View.VISIBLE
anim.start()
if (menuContainer.isAttachedToWindow) {
val cx = menuContainer.measuredWidth - fabSize / 2
val cy = menuContainer.measuredHeight - fabSize / 2
val finalRadius = max(menuContainer.width, menuContainer.height)
val anim = ViewAnimationUtils.createCircularReveal(menuContainer, cx, cy, 0f, finalRadius.toFloat())
anim.duration = 150
menuContainer.visibility = View.VISIBLE
anim.start()
}
}
/**
@ -145,11 +149,17 @@ class NewDataMenuActivity : BaseActivity() {
anim.duration = 150
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
menuContainer.visibility = View.INVISIBLE
finish()
}
// override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
// super.onAnimationEnd(animation, isReverse)
// menuContainer.visibility = View.INVISIBLE
// finish()
// }
})
anim.start()

@ -15,7 +15,7 @@ import timber.log.Timber
class FilterDetailsViewModelFactory(var filter: Filter, private var categoryRow: FilterCategoryRow): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return FilterDetailsViewModel(categoryRow, filter) as T
}
@ -31,7 +31,7 @@ class FilterDetailsViewModel(categoryRow: FilterCategoryRow, var filter: Filter)
this.defineSelectedItems()
}
override fun adapterRows(): List<RowRepresentable>? {
override fun adapterRows(): List<RowRepresentable> {
return this.rows
}

@ -3,7 +3,10 @@ package net.pokeranalytics.android.ui.modules.handhistory.editor
import android.content.res.ColorStateList
import android.text.InputType
import android.view.*
import android.widget.*
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.core.view.isEmpty
import androidx.recyclerview.widget.LinearLayoutManager
@ -145,7 +148,7 @@ class EditorAdapter(
}
}
abstract inner class RowHandHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder {
abstract inner class AbstractRowHandHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder {
private var currentPosition = 0
@ -195,25 +198,25 @@ class EditorAdapter(
// hides soft input view,
// 22/06/22: does not seem useful as - I believe - manifest activity has android:windowSoftInputMode="stateAlwaysHidden"
editText.setTextIsSelectable(true)
// Enabled
//
// // Enabled
val isEnabled = adapter.dataSource.isEnabled(row, tag)
editText.isEnabled = isEnabled
// Text
//
// // Text
val string = adapter.dataSource.charSequenceForRow(row, itemView.context, tag)
editText.setText(string)
// Focus
//
// // Focus
val isFocused = adapter.dataSource.isSelected(position, row, tag)
toggleFocus(editText, isFocused)
editText.isFocusable = adapter.dataSource.isFocusable(position, row, tag)
editText.isFocusableInTouchMode = adapter.dataSource.isFocusable(position, row, tag)
// Put cursor at the end
if (isFocused) {
editText.setSelection(editText.text.length)
}
// toggleFocus(editText, isFocused)
// editText.isFocusable = adapter.dataSource.isFocusable(position, row, tag)
// editText.isFocusableInTouchMode = adapter.dataSource.isFocusable(position, row, tag)
//
// // Put cursor at the end
// if (isFocused) {
// editText.setSelection(editText.text.length)
// }
// Background
setViewBackground(editText, isEnabled, isFocused)
@ -242,7 +245,7 @@ class EditorAdapter(
editText.isFocusableInTouchMode = true
editText.setOnTouchListener { _, event ->
editText.setOnTouchListener { view, event ->
if (event.action == MotionEvent.ACTION_UP) {
// Both are required, otherwise requestFocus() fails
@ -253,6 +256,8 @@ class EditorAdapter(
editTextSelected(editText)
}
view.performClick()
return@setOnTouchListener true
}
@ -357,7 +362,7 @@ class EditorAdapter(
layout.setOnClickListener {
if ((dataSource as EditorViewModel).isEdited) {
delegate?.onRowSelected(this.currentPosition, row, layout.tag as Int)
setViewBackground(layout, true, true)
setViewBackground(layout, isEnabled = true, isFocused = true)
}
}
@ -367,7 +372,7 @@ class EditorAdapter(
}
inner class RowStraddleHolder(itemView: View) : RowHandHolder(itemView) {
inner class RowStraddleHolder(itemView: View) : AbstractRowHandHolder(itemView) {
private var container: ViewGroup = itemView.findViewById(R.id.container)
private var positionsChipGroup: FlowLayout = itemView.findViewById(R.id.positionsChipGroup)
@ -409,7 +414,7 @@ class EditorAdapter(
}
}
inner class RowPositionHolder(itemView: View) : RowHandHolder(itemView) {
inner class RowPositionHolder(itemView: View) : AbstractRowHandHolder(itemView) {
private var recycler: RecyclerView = itemView.findViewById(R.id.recycler)
private var positionAdapter: PositionAdapter = PositionAdapter()
@ -445,7 +450,7 @@ class EditorAdapter(
}
inner class RowActionReadHolder(itemView: View) : RowHandHolder(itemView) {
inner class RowActionReadHolder(itemView: View) : AbstractRowHandHolder(itemView) {
private var playerImage: PlayerImageView = itemView.findViewById(R.id.player_image_rhar)
private var stackText: TextView = itemView.findViewById(R.id.stackText)
@ -481,7 +486,7 @@ class EditorAdapter(
/**
* Display a hand action
*/
inner class RowActionHolder(itemView: View) : RowHandHolder(itemView) {
inner class RowActionHolder(itemView: View) : AbstractRowHandHolder(itemView) {
private var playerImage: PlayerImageView = itemView.findViewById(R.id.player_image_rha)
private var actionButton: AppCompatButton = itemView.findViewById(R.id.actionButton)
@ -503,18 +508,19 @@ class EditorAdapter(
amountEditText.inputType = InputType.TYPE_NUMBER_FLAG_DECIMAL
amountEditText.isFocusableInTouchMode = true
amountEditText.setOnTouchListener { _, event ->
amountEditText.setOnTouchListener { view, event ->
// Timber.d("=== event.action = ${event.action}")
if (event.action == MotionEvent.ACTION_UP) {
// Both are required, otherwise requestFocus() fails
amountEditText.isFocusable = true
amountEditText.isFocusableInTouchMode = true
amountEditText.requestFocus()
// amountEditText.isFocusable = true
// amountEditText.isFocusableInTouchMode = true
//
// amountEditText.requestFocus()
editTextSelected(amountEditText)
}
view.performClick()
return@setOnTouchListener true
}
@ -568,7 +574,7 @@ class EditorAdapter(
/**
* Display a hand street
*/
inner class RowStreetHolder(itemView: View) : RowHandHolder(itemView) {
inner class RowStreetHolder(itemView: View) : AbstractRowHandHolder(itemView) {
private var cardsLayout: LinearLayout = itemView.findViewById(R.id.cardsLayout)
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
@ -586,7 +592,7 @@ class EditorAdapter(
/**
* Display a hand action
*/
inner class RowPlayerSummaryHolder(itemView: View) : RowHandHolder(itemView) {
inner class RowPlayerSummaryHolder(itemView: View) : AbstractRowHandHolder(itemView) {
private var hpsPlayerImage: PlayerImageView = itemView.findViewById(R.id.hps_player_image)
private var handLayout: LinearLayout = itemView.findViewById(R.id.handLayout)
@ -613,7 +619,7 @@ class EditorAdapter(
}
}
abstract inner class AbstractRowPlayerSetup(itemView: View) : RowHandHolder(itemView) {
abstract inner class AbstractRowPlayerSetup(itemView: View) : AbstractRowHandHolder(itemView) {
private var psHandLayout: LinearLayout = itemView.findViewById(R.id.ps_hand_layout)
private var positionButton: AppCompatButton = itemView.findViewById(R.id.position_button)

@ -317,7 +317,7 @@ class EditorFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListen
val handRow = this.model.rowRepresentableForPosition(selection.index) as? HandHistoryRow
val holder =
binding.recyclerView.findViewHolderForAdapterPosition(selection.index) as? EditorAdapter.RowHandHolder
binding.recyclerView.findViewHolderForAdapterPosition(selection.index) as? EditorAdapter.AbstractRowHandHolder
holder?.let {
val amountEditText = it.editTextForTag(selection.tag)
this.binding.keyboard.setAmountEditText(

@ -117,7 +117,8 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
type = if (significant != null) {
val betAmount = significant.action.amount
val remainingStack = computedAction.stackBeforeActing
if (remainingStack != null && betAmount != null && remainingStack < betAmount) {
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0
if (remainingStack != null && betAmount != null && (committedStack + remainingStack < betAmount)) {
Action.Type.CALL_ALLIN
} else {
Action.Type.RAISE_ALLIN
@ -130,8 +131,10 @@ 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 && remainingStack < betAmount) {
if (remainingStack != null && committedStack + remainingStack < betAmount) {
type = Action.Type.CALL_ALLIN
}
} ?: throw PAIllegalStateException("Can't call without a significant action")
@ -219,7 +222,8 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
}
}
Action.Type.BET, Action.Type.POT, Action.Type.RAISE -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0
if (remainingStack != null && actionAmount != null && committedStack + remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL)
} else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
@ -228,7 +232,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
Action.Type.RAISE_ALLIN, Action.Type.BET_ALLIN -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL)
} else if (activePositions(index).size == 2 && remainingStack != null && actionAmount != null && remainingStack > actionAmount) {
} else if (activePositions(index).size == 1 && remainingStack != null && actionAmount != null && remainingStack > actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL)
} else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
@ -347,8 +351,9 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
val activePositions = activePositions(refIndex)
// 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())) {
// 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())) {
activePositions.remove(refIndexPosition)
}
@ -523,7 +528,8 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
created = true
}
val stack = this.filter { it.positionIndex == positionIndex }.sumByDouble { it.action.effectiveAmount }
val stack =
this.filter { it.positionIndex == positionIndex }.sumOf { it.action.effectiveAmount }
playerSetup.stack = stack
if (created) {
@ -595,7 +601,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)
@ -609,7 +615,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
}
override fun totalPotSize(index: Int): Double {
return this.handHistory.anteSum + this.take(index).sumByDouble { it.action.effectiveAmount }
return this.handHistory.anteSum + this.take(index).sumOf { it.action.effectiveAmount }
}
/***

@ -916,7 +916,7 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
this.handSetup.setStraddlePositions(this.firstStraddlePosition!!, positions)
}
this.handHistory.configure(this.handSetup) // restart initial setup
this.handHistory.configure(this.handSetup, true) // restart initial setup
this.sortedActions.load(this.handHistory) // recreate the sorted Actions
this.createRowRepresentation() // make the table rows
@ -1058,6 +1058,7 @@ 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(150),
GATHER_ANIMATION(2),
DISTRIBUTION_ANIMATION(2)
STATE(50),
GATHER_ANIMATION(1),
DISTRIBUTION_ANIMATION(1)
}
class FrameManager {

@ -3,7 +3,13 @@ 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
@ -11,13 +17,14 @@ 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.*
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
@ -25,7 +32,7 @@ import net.pokeranalytics.android.util.video.AnimatedGIFWriter
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.util.*
import java.util.Date
import kotlin.coroutines.CoroutineContext
enum class FileType(var value: String) {
@ -52,11 +59,7 @@ class ReplayExportService : Service() {
fun videoExport(handHistoryId: String) {
this@ReplayExportService.handHistoryId = handHistoryId
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startFFMPEGVideoExport()
} else {
startFFMPEGVideoExportPreQ()
}
startFFMPEGVideoExport()
}
fun gifExport(handHistoryId: String) {
@ -146,6 +149,8 @@ class ReplayExportService : Service() {
private fun startFFMPEGVideoExport() {
val start = Date().time
GlobalScope.launch(coroutineContext) {
val async = GlobalScope.async {
@ -157,7 +162,6 @@ class ReplayExportService : Service() {
val animator = ReplayerAnimator(handHistory, true)
val square = 1024
val width = square
val height = square
@ -165,120 +169,212 @@ 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 output = "${outputDirectory.path}/$fileName"
Timber.d("Assembling images for video...")
val outputFile = File(outputDirectory, fileName)
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) {
Timber.d("Creating video with MediaMuxer...")
when {
it.returnCode.isSuccess -> {
Timber.d("FFMPEG command execution completed successfully")
}
it.returnCode.isCancel -> {
Timber.d("Command execution cancelled by user.")
}
else -> {
Timber.d(String.format("Command execution failed with rc=%d and the output below.", it.returnCode.value))
}
}
File(dpath).delete()
tmpDir.delete()
val file = File(output)
try {
createVideoWithMediaMuxer(animator, context, outputFile, width, height)
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.Images.Media.MIME_TYPE, FileType.VIDEO_MP4.value)
put(MediaStore.Video.Media.MIME_TYPE, FileType.VIDEO_MP4.value)
}
// copy video to nice path
resolver.insert(videoCollection, fileDetails)?.let { uri ->
Timber.d("copy file at uri = $uri")
val os = resolver.openOutputStream(uri)
os?.write(file.readBytes())
os?.write(outputFile.readBytes())
os?.close()
file.delete() // delete temp file
outputFile.delete() // delete temp file
notifyUser(uri, FileType.VIDEO_MP4)
val end = Date().time
Timber.d("video generation duration = ${end - start}")
} ?: run {
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 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 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 startGIFExportPreQ() {
@ -342,80 +438,6 @@ class ReplayExportService : Service() {
}
private fun startFFMPEGVideoExportPreQ() {
GlobalScope.launch(coroutineContext) {
val async = 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.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)
@ -454,7 +476,7 @@ class ReplayExportService : Service() {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(intent, getString(R.string.open_file_with))
val pendingIntent = PendingIntent.getActivity(this, 0, chooser, PendingIntent.FLAG_CANCEL_CURRENT)
val pendingIntent = PendingIntent.getActivity(this, 0, chooser, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
TriggerNotification(this, title, body, pendingIntent)

@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.RectF
import kotlinx.coroutines.android.awaitFrame
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Action
@ -97,7 +96,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
* 2/ The last frame of the last step is longer because some video players
* auto-replay videos, and we want viewers to visualize the end.
*/
private val visualOccurences: Int
private val visualOccurrences: Int
get() {
val step = this.currentStep
return when {
@ -516,7 +515,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
* Generates images and image descriptor to build the video using ffmpeg
* Command line: https://trac.ffmpeg.org/wiki/Slideshow
*/
suspend fun generateVideoContent(context: Context): File {
fun generateVideoContent(context: Context): File {
var ffmpegImageDescriptor = ""
var vo = 0.0
@ -536,7 +535,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
val bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
vo = this.visualOccurences / 90.0 // this is needed before the call to drawTable which pass to the next frame
vo = this.visualOccurrences / 90.0 // this is needed before the call to drawTable which pass to the next frame
this.drawer.drawTable(canvas, context)
@ -550,7 +549,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
count++
awaitFrame()
// awaitFrame()
}
nextStep()
}
@ -588,11 +587,12 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
// Timber.d("FRAME [$f] >> step: $currentStepIndex, frame: $currentFrameIndex, vo: ${this.visualOccurences}, type: ${this.frameType}")
val bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val vo = this.visualOccurences // this is needed before the call to drawTable which pass to the next frame
val vo = this.visualOccurrences // this is needed before the call to drawTable which pass to the next frame
this.drawer.drawTable(canvas, context)
frameHandler(bitmap, vo)
bitmap.recycle()
}

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

@ -1,7 +1,10 @@
package net.pokeranalytics.android.ui.modules.handhistory.replayer
import android.content.Context
import android.graphics.*
import android.graphics.Canvas
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.RectF
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import net.pokeranalytics.android.R
@ -250,7 +253,7 @@ class TableDrawer {
cardRects.forEachIndexed { j, cardRect ->
val showVillainHands = Preferences.getShowVillainCards(context)
if (j < cards?.size ?: 0 && (showVillainHands || isHero)) { // show card
if (j < (cards?.size ?: 0) && (showVillainHands || isHero)) { // show card
val card = cards?.get(j)!! // tested line before
drawCard(card, cardRect, canvas, context)
} else { // show hidden cards

@ -19,7 +19,6 @@ import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.view.GridSpacingItemDecoration
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.noGroupingFormatted
import timber.log.Timber
import java.text.DecimalFormatSymbols
@ -156,8 +155,8 @@ class KeyboardAmountView : AbstractKeyboardView,
this.setInputConnection(editText)
editText.setText(amount?.noGroupingFormatted)
editText.requestFocus()
// editText.setText(amount?.noGroupingFormatted)
// editText.requestFocus()
}

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

@ -11,6 +11,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
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.async
@ -36,7 +37,10 @@ import net.pokeranalytics.android.ui.helpers.DateTimePickerManager
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableDiffCallback
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.Preferences
@ -76,7 +80,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun onResume() {
super.onResume()
CrashLogging.log("session (id=${this.currentSession.id}): valid=${currentSession.isValid}, managed=${currentSession.isManaged}, loaded=${currentSession.isLoaded} ")
// CrashLogging.log("session (id=${this.currentSession.id}): valid=${currentSession.isValid}, managed=${currentSession.isManaged}, loaded=${currentSession.isLoaded} ")
this.refreshTimer()
}
@ -126,10 +130,10 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
setDisplayHomeAsUpEnabled(true)
val viewManager = SmoothScrollLinearLayoutManager(requireContext())
// val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
layoutManager = LinearLayoutManager(requireContext())
}
binding.floatingActionButton.setOnClickListener {

@ -4,7 +4,6 @@ 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
@ -50,8 +49,8 @@ class DealtHandsPerHourFragment : RealmFragment() {
setDisplayHomeAsUpEnabled(true)
val userConfig = UserConfig.getConfiguration(this.getRealm())
this.liveValue.hint = "${userConfig.liveDealtHandsPerHour}"
this.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}"
this.binding.liveValue.hint = "${userConfig.liveDealtHandsPerHour}"
this.binding.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}"
}
@ -60,10 +59,10 @@ class DealtHandsPerHourFragment : RealmFragment() {
getRealm().executeTransaction { realm ->
val userConfig = UserConfig.getConfiguration(realm)
this.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour ->
this.binding.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour ->
userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour
}
this.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour ->
this.binding.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour ->
userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour
}
realm.copyToRealmOrUpdate(userConfig)
@ -76,13 +75,15 @@ class DealtHandsPerHourFragment : RealmFragment() {
}
}
this.liveValue.clearFocus()
this.onlineValue.clearFocus()
this.binding.liveValue.clearFocus()
this.binding.onlineValue.clearFocus()
// Hides keyboard
val imm: InputMethodManager =
requireContext().getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view!!.windowToken, 0)
this.activity?.finish()
}
}

@ -27,14 +27,6 @@ data class DefaultLegendValues(
*/
open class LegendView : FrameLayout {
// open class Values(var titleResId: String, var leftFormat: TextFormat, var rightFormat: TextFormat? = null)
// class MultiLineValues(
// var firstTitle: String,
// var secondTitle: String,
// leftFormat: TextFormat,
// rightFormat: TextFormat? = null
// ) : Values("", leftFormat, rightFormat)
protected lateinit var legendLayout: ConstraintLayout
/**

@ -14,9 +14,11 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat.getColor
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.net.toUri
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Player
import net.pokeranalytics.android.ui.extensions.px
import timber.log.Timber
/**
@ -91,17 +93,19 @@ class PlayerImageView : FrameLayout {
// Picture
player.picture?.let { picture ->
val rDrawable = RoundedBitmapDrawableFactory.create(resources, picture)
rDrawable.isCircular = true
this.playerImage.setImageDrawable(rDrawable)
} ?: run {
Timber.d("picture = $picture")
if (picture.startsWith("content://")) {
this.playerImage.setImageURI(picture.toUri())
} else { // older way
val rDrawable = RoundedBitmapDrawableFactory.create(resources, picture)
rDrawable.isCircular = true
this.playerImage.setImageDrawable(rDrawable)
}
} ?: run { // no pic
this.playerStroke.background = ResourcesCompat.getDrawable(resources, R.drawable.circle_stroke_kaki, null)
this.playerImage.setImageDrawable(null)
this.playerInitial.text = player.initials
this.playerInitial.setTextSize(TypedValue.COMPLEX_UNIT_SP, size.getFontSize())
}
// Player color
@ -109,7 +113,7 @@ class PlayerImageView : FrameLayout {
player.color != null -> player.color as Int
player.hasPicture() -> Color.TRANSPARENT
isHero -> getColor(context, R.color.kaki_lighter)
else -> getColor(context, R.color.kaki)
else -> getColor(context, R.color.kaki_medium)
}
// Stroke & initial

@ -17,7 +17,7 @@ import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.*
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import kotlinx.android.synthetic.main.cell_calendar_time_unit.view.*
import com.google.android.material.tabs.TabLayout
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat
@ -96,6 +96,7 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
HAND_HISTORY(R.layout.row_hand_history_view),
CALENDAR_GRID_CELL(R.layout.cell_calendar_grid),
CALENDAR_TIME_UNIT_CELL(R.layout.cell_calendar_time_unit),
ROW_TAB(R.layout.row_tab),
// ROW_HAND_ACTION(R.layout.row_hand_action),
// ROW_HAND_STREET(R.layout.row_hand_cards),
@ -162,6 +163,8 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
CALENDAR_GRID_CELL -> CalendarGridCellHolder(layout)
CALENDAR_TIME_UNIT_CELL -> CalendarTimeUnitCellHolder(layout)
ROW_TAB -> RowTabViewHolder(layout)
// Separator
SEPARATOR -> SeparatorViewHolder(layout)
@ -442,6 +445,29 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
}
}
/**
* Display a button in a row
*/
inner class RowTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
BindableHolder {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
val tabLayout = itemView.findViewById<TabLayout>(R.id.tabs)
val listener = object: TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
adapter.delegate?.onRowValueChanged(tabLayout.selectedTabPosition, row)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}
tabLayout.addOnTabSelectedListener(listener)
}
}
/**
* Display a session view
*/
@ -663,7 +689,8 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
if (row is CellResult) {
itemView.timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background)
val timeUnit = itemView.findViewById<View>(R.id.timeUnit)
timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background)
}
}
@ -675,17 +702,22 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
BindableHolder {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
if (row is PerformanceRow) {
itemView.findViewById<AppCompatTextView>(R.id.title)?.let {
it.text = row.localizedTitle(itemView.context)
}
itemView.findViewById<AppCompatTextView>(R.id.value)?.let {
it.text = adapter.dataSource.charSequenceForRow(row, itemView.context)
}
itemView.findViewById<AppCompatImageView>(R.id.badge)?.let {
it.isVisible = adapter.dataSource.boolForRow(row)
}
itemView.findViewById<AppCompatTextView>(R.id.title)?.let {
it.text = row.localizedTitle(itemView.context)
}
itemView.findViewById<AppCompatTextView>(R.id.value)?.let {
it.text = adapter.dataSource.charSequenceForRow(row, itemView.context)
}
itemView.findViewById<AppCompatImageView>(R.id.badge)?.let {
it.isVisible = adapter.dataSource.boolForRow(row)
}
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
itemView.setOnClickListener(listener)
if (row is PerformanceRow) {
itemView.findViewById<AppCompatImageView>(R.id.nextArrow)?.let {
it.visibility = if (row.report.hasGraph) {
View.VISIBLE
@ -693,11 +725,6 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
View.GONE
}
}
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
itemView.setOnClickListener(listener)
}
}
}

@ -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 kotlinx.android.synthetic.main.view_keyboard_stakes.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.ViewKeyboardStakesBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import java.text.DecimalFormatSymbols
@ -16,6 +16,9 @@ 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)
}
@ -30,42 +33,51 @@ 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)
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 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 layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
addView(view, layoutParams)
// addView(binding, layoutParams)
// addView(view, layoutParams)
}
fun setSeparatorVisibility(visible: Boolean) {
binding.valueSeparator.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun commitText(string: String) {

@ -105,7 +105,7 @@ open class FixedValueFilterItemRow(queryCondition: QueryCondition,
get() { return rawCondition }
override fun valueFormatted(context: Context): CharSequence? {
throw PAIllegalStateException("Not applicable for $rawCondition")
return null //throw PAIllegalStateException("Not applicable for $rawCondition")
}
override val singleValue: Any?

@ -12,12 +12,13 @@ import net.pokeranalytics.android.ui.view.RowViewType
enum class PlayerPropertiesRow : RowRepresentable {
IMAGE,
NAME,
SUMMARY;
SUMMARY,
TAB_SELECTOR;
override val resId: Int?
get() {
return when (this) {
IMAGE -> null
IMAGE, TAB_SELECTOR -> null
NAME -> R.string.name
SUMMARY -> R.string.summary
}
@ -29,13 +30,14 @@ enum class PlayerPropertiesRow : RowRepresentable {
IMAGE -> RowViewType.ROW_PLAYER_IMAGE.ordinal
NAME -> RowViewType.TITLE_SUBTITLE.ordinal
SUMMARY -> RowViewType.TITLE_SUBTITLE.ordinal
TAB_SELECTOR -> RowViewType.ROW_TAB.ordinal
}
}
override val bottomSheetType: BottomSheetType
get() {
return when (this) {
IMAGE -> BottomSheetType.NONE
IMAGE, TAB_SELECTOR -> BottomSheetType.NONE
NAME -> BottomSheetType.EDIT_TEXT
SUMMARY -> BottomSheetType.EDIT_TEXT_MULTI_LINES
}
@ -45,25 +47,4 @@ enum class PlayerPropertiesRow : RowRepresentable {
return null
}
// override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
//
//
// }
// override fun startEditing(dataSource: Any?, parent: Fragment?) {
// if (dataSource == null) return
// if (dataSource !is Player) return
// if (parent == null) return
// if (parent !is RowRepresentableDelegate) return
// val data = RowEditableDataSource()
// when (this) {
// NAME -> data.append(dataSource.name)
// SUMMARY -> data.append(dataSource.summary)
// else -> PokerAnalyticsException.InputFragmentException
// }
//
// InputFragment.buildAndShow(this, parent, data)
// }
}

@ -37,6 +37,7 @@ enum class SettingsRow : RowRepresentable {
CURRENCY,
DEALT_HANDS_PER_HOUR,
SHOW_INAPP_BADGES,
BACKUP_EMAIL,
// Export
EXPORT_CSV_SESSIONS,
@ -80,7 +81,7 @@ enum class SettingsRow : RowRepresentable {
rows.addAll(arrayListOf(FOLLOW_US, DISCORD, BLOG_TIPS, SHOULD_SHOW_BLOG_TIPS))
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.preferences))
rows.addAll(arrayListOf(CURRENCY, DEALT_HANDS_PER_HOUR, SHOW_INAPP_BADGES))
rows.addAll(arrayListOf(LANGUAGE, CURRENCY, DEALT_HANDS_PER_HOUR, BACKUP_EMAIL, SHOW_INAPP_BADGES))
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.export))
rows.addAll(arrayListOf(EXPORT_CSV_SESSIONS, EXPORT_CSV_TRANSACTIONS))
@ -131,6 +132,7 @@ enum class SettingsRow : RowRepresentable {
DISCORD -> R.string.join_discord
DEALT_HANDS_PER_HOUR -> R.string.dealt_hands_per_hour
SHOW_INAPP_BADGES -> R.string.show_inapp_badges
BACKUP_EMAIL -> R.string.backup_email
else -> null
}
}
@ -142,6 +144,7 @@ enum class SettingsRow : RowRepresentable {
BANKROLL_REPORT, TOP_10, PLAYERS -> RowViewType.TITLE_ICON_ARROW.ordinal
VERSION, SUBSCRIPTION -> RowViewType.TITLE_VALUE.ordinal
LANGUAGE, CURRENCY -> RowViewType.TITLE_VALUE_ARROW.ordinal
BACKUP_EMAIL -> RowViewType.TITLE_BADGE_VALUE.ordinal
FOLLOW_US -> RowViewType.ROW_FOLLOW_US.ordinal
STOP_NOTIFICATION, SHOULD_SHOW_BLOG_TIPS, SHOW_INAPP_BADGES -> RowViewType.TITLE_SWITCH.ordinal
STOP_NOTIFICATION_MESSAGE -> RowViewType.INFO.ordinal

@ -16,7 +16,7 @@ import java.util.*
class BottomSheetViewModelFactory(var row: RowRepresentable, var delegate: RowRepresentableDelegate): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return BottomSheetViewModel(row) as T
// return modelClass.getConstructor(RowRepresentable::class.java).newInstance(row)
}

@ -0,0 +1,73 @@
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 net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.util.csv.DataType
import timber.log.Timber
import java.util.concurrent.TimeUnit
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 val realm = Realm.getDefaultInstance()
init {
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
}
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
}
}
private fun backupDataType(dataType: DataType) {
val data = Data.Builder()
.putInt(BackupWorker.ParamKeys.DATA.value, dataType.ordinal)
var duration = 10L
var unit = TimeUnit.HOURS
if (BuildConfig.DEBUG) {
duration = 1L
unit = TimeUnit.SECONDS
}
val backupWorker = OneTimeWorkRequestBuilder<BackupWorker>()
.setInitialDelay(duration, unit)
.setInputData(data.build())
.addTag(dataType.workId)
.build()
Timber.d(">>> create backupTask")
WorkManager.getInstance(context).enqueueUniqueWork(dataType.workId, ExistingWorkPolicy.REPLACE, backupWorker)
}
}

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

@ -6,6 +6,7 @@ import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
class FileUtils {
@ -54,4 +55,19 @@ class FileUtils {
}
}
fun InputStream.copyStreamToFile(outputFile: File) {
this.use { input ->
val outputStream = FileOutputStream(outputFile)
outputStream.use { output ->
val buffer = ByteArray(4 * 1024) // buffer size
while (true) {
val byteCount = input.read(buffer)
if (byteCount < 0) break
output.write(buffer, 0, byteCount)
}
output.flush()
}
}
}

@ -1,19 +1,10 @@
package net.pokeranalytics.android.util
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.graphics.Paint.FILTER_BITMAP_FLAG
import android.media.ExifInterface
import android.net.Uri
import android.os.Environment
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -23,78 +14,7 @@ import java.util.*
object ImageUtils {
/**
* Rotate a bitmap if it's necessary (depending of the EXIF data)
* Some devices don't rotate the picture but instead add the orientation
* value in the EXIF data.
* That's why we need sometimes to rotate by ourselves the bitmap
*
* @param src The file to check (for getting the Exif data)
* @param bitmap The bitmap to modify (if necessary)
* @return The bitmap in the correct orientation
*/
fun rotateBitmap(src: String, bitmap: Bitmap, updateFile: Boolean): Bitmap {
try {
val orientation = getExifOrientation(src)
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
return bitmap
}
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
matrix.setRotate(180f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.setRotate(90f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.setRotate(-90f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
else -> return bitmap
}
try {
val oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bitmap.recycle()
if (updateFile) {
updateFile(src, oriented)
}
return oriented
} catch (e: OutOfMemoryError) {
e.printStackTrace()
return bitmap
}
} catch (e: IOException) {
e.printStackTrace()
}
return bitmap
}
/**
* Get the Exif orientation value
*
* @param filePath The path of the file
* @return the orientation value
* @throws IOException
*/
@Throws(IOException::class)
private fun getExifOrientation(filePath: String): Int {
val exifInterface = ExifInterface(filePath)
return exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
}
/**
* Save a bitmap into a file (& apply 90% compression)
*
@ -117,46 +37,7 @@ object ImageUtils {
}
}
/**
* Resize a file with the given maximum width or height (and keep the ratio!)
* @param filePath String: File path
* @param bitmap Bitmap: Image
* @param maxWidth int: Max width
* @param maxHeight int: Max height
*/
fun resizeFile(filePath: String, bitmap: Bitmap, maxWidth: Int, maxHeight: Int) {
var bm = bitmap
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
val imageWidth = options.outWidth
val imageHeight = options.outHeight
var newWidth: Int
var newHeight: Int
if (imageWidth > imageHeight) {
newWidth = maxWidth
newHeight = imageHeight * maxWidth / imageWidth
if (newHeight > maxHeight) {
newHeight = maxHeight
newWidth = imageWidth * maxHeight / imageHeight
}
} else {
newHeight = maxHeight
newWidth = imageWidth * maxHeight / imageHeight
if (newWidth > maxWidth) {
newWidth = maxWidth
newHeight = imageHeight * maxWidth / imageWidth
}
}
bm = Bitmap.createScaledBitmap(bm, newWidth, newHeight, true)
updateFile(filePath, bm)
}
/**
* Create a unique temp image file name
*
@ -266,37 +147,4 @@ object ImageUtils {
}
/**
* Save the bitmap in a file
*/
fun saveBitmapInFile(context: Context, bitmap: Bitmap, filename: String, action: (filePath: String) -> Unit) {
GlobalScope.launch {
val outputFile = File(context.filesDir, filename)
var out: FileOutputStream? = null
try {
out = FileOutputStream(outputFile.absolutePath)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) // bmp is your Bitmap instance
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
if (out != null) {
out.close()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
GlobalScope.launch(Dispatchers.Main) {
Timber.d("Save file here: ${outputFile.absolutePath}")
action(outputFile.absolutePath)
}
}
}
}

@ -0,0 +1,32 @@
package net.pokeranalytics.android.util
import java.util.*
enum class Language(val code: String) {
ENGLISH("en"),
FRENCH("fr"),
GERMAN("de"),
HINDI("hi"),
ITALIAN("it"),
JAPANESE("ja"),
PORTUGUESE("pt"),
RUSSIAN("ru"),
CHINESE("zh");
private val localized: String
get() {
return Locale(code).displayLanguage
}
private val localName: String
get() {
val locale = Locale(code)
return locale.getDisplayLanguage(locale)
}
val dualNames: String
get() {
return "$localized / $localName"
}
}

@ -175,13 +175,14 @@ 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?.let {
if (!locationResultReceived && it.isNotEmpty()) {
locationResult.locations.firstOrNull()?.let { location ->
if (!locationResultReceived) {
locationResultReceived = true
callback(it.first())
callback(location)
fusedLocationClient.removeLocationUpdates(this)
}
}
@ -194,7 +195,12 @@ class LocationManager(private var context: Context) {
locationRequest.interval = 200L
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
Looper.myLooper()?.let { looper ->
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, looper)
} ?: run {
callback.invoke(null)
}
} else {
// 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"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions_v2"),
PATCH_ZERO_TABLE("patchZeroTable"),
SHOW_STOP_NOTIFICATIONS("showStopNotifications"),
ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"),
@ -49,7 +49,11 @@ class Preferences {
CLEAN_BLINDS_FILTERS("deleteBlindsFilters"),
SHOW_IN_APP_BADGES("showInAppBadges"),
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"),
PATCH_RATED_AMOUNT("patchRatedAmount[new field]")
PATCH_RATED_AMOUNT("patchRatedAmount[new field]"),
BACKUP_EMAIL("backupEmail"),
LANGUAGE_CODE("languageCode"),
SESSIONS_BACKUP_SUCCESS("sessionsBackupSuccess"),
TRANSACTIONS_BACKUP_SUCCESS("transactionsBackupSuccess")
}
enum class FeedMessage {
@ -327,6 +331,44 @@ class Preferences {
setLong(Keys.LAST_CALENDAR_BADGE_DATE, date, context)
}
fun setBackupEmail(email: String, context: Context) {
setString(Keys.BACKUP_EMAIL, email, context)
}
fun getBackupEmail(context: Context): String? {
return getString(Keys.BACKUP_EMAIL, context)
}
fun hasBackupEmail(context: Context): Boolean {
getString(Keys.BACKUP_EMAIL, context)?.let {
return it.isNotEmpty()
}
return false
}
fun setLanguageCode(languageCode: String, context: Context) {
setString(Keys.LANGUAGE_CODE, languageCode, context)
}
fun getLanguageCode(context: Context): String? {
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)
}
}
}

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm
import io.realm.kotlin.deleteFromRealm
import net.pokeranalytics.android.model.interfaces.Identifiable
@ -7,7 +8,6 @@ import net.pokeranalytics.android.model.interfaces.ObjectIdentifier
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.extensions.findById
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
/**
* The various sources of CSV
@ -18,7 +18,8 @@ enum class DataSource {
POKER_INCOME,
POKER_BANKROLL_TRACKER,
RUN_GOOD,
POKER_AGENT;
POKER_AGENT,
POKER_BASE;
val availableDateFormats: List<String>
get() {
@ -28,6 +29,14 @@ enum class DataSource {
}
}
val delimiter: Char
get() {
return when (this) {
POKER_BASE -> ';'
else -> ','
}
}
}
/**
@ -40,11 +49,11 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
*/
private val realmModelIds = mutableListOf<ObjectIdentifier>()
abstract fun parseData(realm: Realm, record: CSVRecord): T?
abstract fun parseData(realm: Realm, record: CSVRecord, context: Context): T?
override fun parse(realm: Realm, record: CSVRecord): Int {
override fun parse(realm: Realm, record: CSVRecord, context: Context): Int {
val data = this.parseData(realm, record)
val data = this.parseData(realm, record, context)
data?.let {
// Timber.d(">>>>>>> identifier added: ${it.id}")
this.realmModelIds.add(it.objectIdentifier)
@ -144,7 +153,7 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
/**
* Method called when iterating on a CSVRecord
*/
abstract fun parse(realm: Realm, record: CSVRecord): Int
abstract fun parse(realm: Realm, record: CSVRecord, context: Context): Int
open fun save(realm: Realm) {
@ -171,7 +180,7 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
}
}
val mandatoryFields = this.fields.filter { !it.optional }
Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count")
// Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count")
return count >= mandatoryFields.size
}

@ -1,16 +1,15 @@
package net.pokeranalytics.android.util.csv
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.extensions.count
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.io.FileReader
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader
@ -23,12 +22,12 @@ interface ImportDelegate {
/**
* A CSVImporter is a class in charge of parsing a CSV file and processing it
* When starting the parsing of a file, the instance will search for a CSVDescriptor, which describes
* the format of a CSV file.
* When finding a descriptor, the CSVImporter then continue to parse the file and delegates the parsing of each row
* to the CSVDescriptor
* When starting the parsing of a file, the instance will search for a CSVDescriptor,
* which describes the format of a CSV file.
* When finding a descriptor, the CSVImporter then continue to parse the file and
* delegates the parsing of each row to the CSVDescriptor
*/
open class CSVImporter(istream: InputStream) {
open class CSVImporter(uri: Uri, var context: Context) {
/**
* The object being notified of the import progress
@ -52,10 +51,11 @@ open class CSVImporter(istream: InputStream) {
* The path of the CSV file
*/
private var path: String? = null
/**
* The InputStream containing a file content
* The Uri of the file to import
*/
private var inputStream: InputStream? = istream
private var uri: Uri? = uri
/**
* The current number of attempts at finding a valid CSVDescriptor
@ -85,97 +85,117 @@ open class CSVImporter(istream: InputStream) {
/**
* The CSV parser
*/
private lateinit var parser: CSVParser
// private var parser: CSVParser? = null
val reader: Reader
get() {
this.uri?.let { uri ->
// it's required to open the stream each time we start a new parser
val inputStream = context.contentResolver.openInputStream(uri)
return InputStreamReader(inputStream)
}
return if (this.path != null) {
FileReader(this.path)
} else {
throw PAIllegalStateException("No data source")
}
}
/**
* Constructs a CSVParser object and starts parsing the CSV
*/
fun start() {
Timber.d("Starting import...")
val realm = Realm.getDefaultInstance()
realm.beginTransaction()
var reader: Reader? = null
if (this.path != null) {
reader = FileReader(this.path)
}
if (this.inputStream != null) {
reader = InputStreamReader(this.inputStream)
}
val descriptorsByDelimiter = ProductCSVDescriptors.all.groupBy { it.source.delimiter }
this.parser = CSVFormat.DEFAULT.withAllowMissingColumnNames().parse(reader)
for ((delimiter, descriptors) in descriptorsByDelimiter) {
Timber.d("Starting import...")
if (this.currentDescriptor != null) {
break
}
realm.beginTransaction()
Timber.d("====== Trying with delimiter '$delimiter'")
val format = CSVFormat.DEFAULT
.withAllowMissingColumnNames()
.withDelimiter(delimiter)
val parser = format.parse(this.reader)
this.parser.forEachIndexed { index, record ->
Timber.d("parse delim = ${format.delimiter}")
parser.forEachIndexed { index, record ->
// Timber.d("line $index")
this.notifyDelegate()
this.notifyDelegate()
if (this.currentDescriptor == null) { // find descriptor
this.currentDescriptor = this.findDescriptor(record)
if (this.currentDescriptor == null) { // find descriptor
this.currentDescriptor = this.findDescriptor(record, descriptors)
this.currentDescriptor?.hasMatched(realm, record)
this.currentDescriptor?.hasMatched(realm, record)
if (this.currentDescriptor == null) {
if (this.currentDescriptor == null) {
if (record.size() >= VALID_RECORD_COLUMNS) {
this.descriptorFindingAttempts++
}
if (this.descriptorFindingAttempts >= VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION) {
realm.cancelTransaction()
realm.close()
throw ImportException("This type of file is not supported")
if (record.size() >= VALID_RECORD_COLUMNS) {
this.descriptorFindingAttempts++
}
if (this.descriptorFindingAttempts >= VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION) {
realm.cancelTransaction()
realm.close()
throw ImportException("This type of file is not supported")
}
}
}
} else { // parse
} else { // parse
// batch commit
val parsingIndex = index + 1
if (parsingIndex % COMMIT_FREQUENCY == 0) {
Timber.d("****** committing at $parsingIndex sessions...")
realm.commitTransaction()
realm.beginTransaction()
}
// batch commit
val parsingIndex = index + 1
if (parsingIndex % COMMIT_FREQUENCY == 0) {
Timber.d("****** committing at $parsingIndex sessions...")
realm.commitTransaction()
realm.beginTransaction()
}
this.currentDescriptor?.let {
if (record.size() == 0) {
this.usedDescriptors.add(it)
this.currentDescriptor =
null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file)
this.descriptorFindingAttempts = 0
} else {
this.currentDescriptor?.let {
if (record.size() == 0) {
this.usedDescriptors.add(it)
this.currentDescriptor =
null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file)
this.descriptorFindingAttempts = 0
} else {
try {
val count = it.parse(realm, record)
try {
val count = it.parse(realm, record, this.context)
this.importedRecords += count
this.totalParsedRecords++
this.importedRecords += count
this.totalParsedRecords++
this.notifyDelegate()
this.notifyDelegate()
} catch (e: Exception) {
this.delegate?.exceptionCaught(e)
}
} catch (e: Exception) {
this.delegate?.exceptionCaught(e)
}
}
} ?: run {
realm.cancelTransaction()
realm.close()
throw ImportException("CSVDescriptor should never be null here")
}
} ?: run {
realm.cancelTransaction()
realm.close()
throw ImportException("CSVDescriptor should never be null here")
}
}
parser.close()
}
Timber.d("Ending import...")
this.notifyDelegate()
realm.commitTransaction()
Timber.d("Ending import...")
realm.close()
}
@ -188,9 +208,9 @@ open class CSVImporter(istream: InputStream) {
/**
* Search for a descriptor in the list of managed formats
*/
private fun findDescriptor(record: CSVRecord): CSVDescriptor? {
private fun findDescriptor(record: CSVRecord, descriptors: List<CSVDescriptor>): CSVDescriptor? {
ProductCSVDescriptors.all.forEach { descriptor ->
descriptors.forEach { descriptor ->
if (descriptor.matches(record)) {
this.currentDescriptor = descriptor
Timber.d("Identified source: ${descriptor.source}")
@ -201,7 +221,7 @@ open class CSVImporter(istream: InputStream) {
}
fun save(realm: Realm) {
this.parser.close()
// this.parser?.close()
realm.refresh()
this.currentDescriptor?.save(realm)
@ -211,7 +231,7 @@ open class CSVImporter(istream: InputStream) {
}
fun cancel(realm: Realm) {
this.parser.close()
// this.parser?.close()
realm.refresh()
this.currentDescriptor?.cancel(realm)

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit
@ -42,7 +43,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
/**
* Parses a [record] and return an optional Session
*/
protected fun parseSession(realm: Realm, record: CSVRecord): Session? {
protected fun parseSession(realm: Realm, record: CSVRecord, context: Context): Session? {
val isTournament = isTournament ?: false
val session = Session.newInstance(realm, isTournament, managed = false)
@ -58,6 +59,7 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
var stackingIn: Double? = null
var stackingOut: Double? = null
var expense: Double? = null
var sb: Double? = null
var bb: Double? = null
@ -216,6 +218,9 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
is SessionField.StackingOut -> {
stackingOut = field.parse(value)
}
is SessionField.Expense -> {
expense = field.parse(value)
}
is SessionField.ListCustomField -> {
val entry = field.customField.getOrCreateEntry(realm, value)
session.customFieldEntries.add(entry)
@ -275,6 +280,11 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!)
this.addAdditionallyCreatedIdentifiable(transaction)
}
if (expense != null && expense != 0.0) {
val type = TransactionType.getOrCreate(realm, TransactionType.Value.EXPENSE, context)
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, expense!!)
this.addAdditionallyCreatedIdentifiable(transaction)
}
return managedSession
} else {

@ -20,7 +20,8 @@ class ProductCSVDescriptors {
pokerAnalytics6iOS,
pokerAnalyticsAndroidSessions,
pokerAnalyticsAndroid6Sessions,
pokerAnalyticsAndroidTransactions
pokerAnalyticsAndroidTransactions,
// pokerBase
)
private val pokerAgent: CSVDescriptor
@ -159,6 +160,21 @@ class ProductCSVDescriptors {
)
}
private val pokerBase: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.POKER_BASE,
null,
SessionField.Start("start", dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
SessionField.End("end", dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
SessionField.Location("location"),
SessionField.Bankroll("currency"),
SessionField.CurrencyCode("currency"),
SessionField.Expense("expenses"),
SessionField.NetResult("profit"),
)
}
private val pokerAnalyticsiOS: SessionCSVDescriptor
get() {
return SessionCSVDescriptor(
@ -210,6 +226,7 @@ class ProductCSVDescriptors {
SessionField.LimitType("Limit"),
SessionField.Game("Game"),
SessionField.TableSize("Table Size"),
SessionField.HandsCount("Hands Count"),
SessionField.Location("Location"),
SessionField.Bankroll("Bankroll"),
SessionField.CurrencyCode("Currency Code"),

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit
@ -30,11 +31,10 @@ class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg el
this.fields.add(f)
}
realm.close()
}
override fun parseData(realm: Realm, record: CSVRecord): Session? {
return this.parseSession(realm, record)
override fun parseData(realm: Realm, record: CSVRecord, context: Context): Session? {
return this.parseSession(realm, record, context)
}
override fun toCSV(data: Session, field: CSVField): String? {

@ -174,6 +174,11 @@ sealed class SessionField {
override var callback: ((String) -> Double?)? = null
) : NumberCSVField
data class Expense(
override var header: String,
override var callback: ((String) -> Double?)? = null
) : NumberCSVField
data class Blind(override var header: String,
override var callback: ((String) -> Pair<Double, Double>?)? = null
) : BlindCSVField

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.realm.Bankroll
@ -10,33 +11,42 @@ import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.util.*
/**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionTransactionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) :
PACSVDescriptor<Identifiable>(source, isTournament, *elements) {
enum class DataType {
TRANSACTION,
SESSION;
private enum class DataType {
TRANSACTION,
SESSION;
companion object {
companion object {
fun valueForString(type: String): DataType? {
return when (type) {
"Deposit/Payout" -> TRANSACTION
"Cash Game", "Tournament" -> SESSION
else -> null
}
}
}
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) {
/**
* Parses a [record] and return an optional Session
*/
override fun parseData(realm: Realm, record: CSVRecord): Identifiable? {
override fun parseData(realm: Realm, record: CSVRecord, context: Context): Identifiable? {
var dataType: DataType? = null
val typeField = fields.firstOrNull { it is SessionField.SessionType }
@ -49,7 +59,7 @@ class SessionTransactionCSVDescriptor(source: DataSource, isTournament: Boolean?
return when (dataType) {
DataType.TRANSACTION -> parseTransaction(realm, record)
else -> parseSession(realm, record)
else -> parseSession(realm, record, context)
}
}

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Bankroll
@ -13,7 +14,7 @@ import java.util.*
class TransactionCSVDescriptor(source: DataSource, vararg elements: CSVField) :
DataCSVDescriptor<Transaction>(source, *elements) {
override fun parseData(realm: Realm, record: CSVRecord): Transaction? {
override fun parseData(realm: Realm, record: CSVRecord, context: Context): Transaction? {
var date: Date? = null
var typeName: String? = null

@ -0,0 +1,15 @@
package net.pokeranalytics.android.util.extensions
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/***
* Returns whether the network is available or not
*/
fun Context.isNetworkAvailable(): Boolean {
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capability = cm.getNetworkCapabilities(cm.activeNetwork)
return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
}

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<!-- <solid android:color="@color/black" />-->
<stroke android:color="@color/black" android:width="3dp"/>
<solid android:color="@color/white"/>
<size
android:width="80dp"
android:height="80dp"/>
</shape>

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="0dp" />
<ImageButton
android:id="@+id/image_capture_button"
android:src="@drawable/circle_border"
style="@style/PokerAnalyticsTheme.TransparentButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".50" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -3,7 +3,8 @@
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:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"

@ -3,7 +3,9 @@
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:layout_height="match_parent"
android:fitsSystemWindows="true"
>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"

@ -20,6 +20,7 @@
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

@ -47,6 +47,7 @@
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"

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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:layout_height="match_parent"
>
<FrameLayout
android:id="@+id/legendContainer"

@ -32,7 +32,6 @@
android:layout_height="match_parent"
android:fillViewport="true"
android:paddingBottom="56dp"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
@ -43,6 +42,7 @@
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

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

Loading…
Cancel
Save