Compare commits

...

95 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
Laurent 1718bea582 Bumps to 6.0.3 3 years ago
  1. 139
      CLAUDE.md
  2. 58
      app/build.gradle
  3. 22
      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. 152
      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: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' //apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android' apply plugin: 'realm-android'
// Crashlytics // Crashlytics
@ -17,8 +17,8 @@ repositories {
android { android {
compileSdkVersion 32 compileSdkVersion 35
buildToolsVersion "30.0.2" buildToolsVersion "30.0.3"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -29,16 +29,13 @@ android {
jvmTarget = JavaVersion.VERSION_1_8 jvmTarget = JavaVersion.VERSION_1_8
} }
lintOptions {
disable 'MissingTranslation'
}
defaultConfig { defaultConfig {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 32 targetSdkVersion 35
versionCode 145 versionCode 180
versionName "6.0.2" versionName "6.0.38"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -91,6 +88,10 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
@ -111,13 +112,16 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews // implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places // Places
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions // Billing / Subscriptions
implementation 'com.android.billingclient:billing:5.0.0' implementation 'com.android.billingclient:billing:7.0.0'
// Import the BoM for the Firebase platform // Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -139,16 +143,36 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export) // ffmpeg for encoding video (HH export)
implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS' // implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera
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 // Instrumented Tests
androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1'
// Test // Test
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.2'
testImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2' testImplementation 'com.android.support.test:rules:1.0.2'

@ -65,3 +65,25 @@
# Enum # 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") Assert.fail("No std100 stat")
} }
results.computedStat(Stat.MAXIMUM_NETRESULT)?.let { results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let {
assertEquals(300.0, it.value, delta) assertEquals(300.0, it.value, delta)
} ?: run { } ?: run {
Assert.fail("No MAXIMUM_NETRESULT") Assert.fail("No MAXIMUM_NETRESULT")
} }
results.computedStat(Stat.MINIMUM_NETRESULT)?.let { results.computedStat(Stat.MINIMUM_NET_RESULT)?.let {
assertEquals(-100.0, it.value, delta) assertEquals(-100.0, it.value, delta)
} ?: run { } ?: run {
Assert.fail("No MINIMUM_NETRESULT") Assert.fail("No MINIMUM_NETRESULT")

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

@ -1,15 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="net.pokeranalytics.android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <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.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <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 <application
android:name=".PokerAnalyticsApplication" android:name=".PokerAnalyticsApplication"
@ -59,135 +62,186 @@
</activity> </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 <activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity" android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity" android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" /> android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity" android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"/> android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity" android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity" android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity" android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity" android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity" android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity" android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard" android:launchMode="standard"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity" android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity" android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity" android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity" android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity" android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity" android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity" android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity" android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog" android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop"/> android:launchMode="singleTop"
android:exported="true"/>
<activity
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 <meta-data
android:name="preloaded_fonts" 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.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -26,6 +24,7 @@ import java.util.*
class PokerAnalyticsApplication : Application() { class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object { companion object {
@ -83,10 +82,14 @@ class PokerAnalyticsApplication : Application() {
// Report // Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext) this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// Infos // Infos
val locale = Locale.getDefault() val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}") CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction { // Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java) // it.delete(Performance::class.java)
// } // }
@ -103,7 +106,7 @@ class PokerAnalyticsApplication : Application() {
realm.close() realm.close()
if (sessionsCount < 10) { if (sessionsCount < 10) {
GlobalScope.launch { CoroutineScope(context = Dispatchers.IO).launch {
FakeDataManager.createFakeSessions(500) 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 package net.pokeranalytics.android.api
import android.content.Context import android.content.Context
import androidx.annotation.Keep
import com.android.volley.VolleyError import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
@ -9,8 +10,11 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
@Keep
@Serializable @Serializable
data class RateResponse(var info: RateInfo) data class RateResponse(var info: RateInfo)
@Keep
@Serializable @Serializable
data class RateInfo(var rate: Double) data class RateInfo(var rate: Double)
@ -23,13 +27,8 @@ class CurrencyConverterApi {
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) { fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
val queue = Volley.newRequestQueue(context) 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" 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") Timber.d("Api call = $url")
val stringRequest = object : StringRequest( 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(tSum / index, stat = AVERAGE, data = session)
results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, 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 / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
results.addEvolutionValue(tBBSum, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue( results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()), (tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO, 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.LiveOnline
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.StaticReport import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -32,7 +33,7 @@ class ReportWhistleBlower(var context: Context) {
private val listeners: MutableList<NewPerformanceListener> = mutableListOf() private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
private var paused: Boolean = false var paused: Boolean = false
private var timer: CountDownTimer? = null private var timer: CountDownTimer? = null
@ -62,9 +63,10 @@ class ReportWhistleBlower(var context: Context) {
} }
fun requestReportLaunch() { fun requestReportLaunch() {
Timber.d(">>> Launch report") // Timber.d(">>> Launch report")
if (paused) { if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return return
} }
@ -131,10 +133,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private var cancelled = false private var cancelled = false
var handler: (() -> Unit)? = null
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
get() = Dispatchers.Default get() = Dispatchers.Default
fun start() { fun start() {
messages.add("Starting task...")
launchReports() launchReports()
} }
@ -142,6 +147,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
this.cancelled = true this.cancelled = true
} }
var messages: MutableList<String> = mutableListOf()
private fun launchReports() { private fun launchReports() {
CoroutineScope(coroutineContext).launch { CoroutineScope(coroutineContext).launch {
@ -171,7 +178,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
private fun launchReport(realm: Realm, report: StaticReport) { private fun launchReport(realm: Realm, report: StaticReport) {
Timber.d(">>> launch report = $report") // Timber.d(">>> launch report = $report")
when (report) { when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, 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) { private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.values().forEach { key -> LiveOnline.entries.forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive) val duration = CashGameOptimalDurationCalculator.start(key.isLive)
analyseOptimalDuration(realm, report, key, duration) analyseOptimalDuration(realm, report, key, duration)
} }
this.handler?.let { it() }
} }
private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) { private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) {
messages.add("Analyse report $staticReport...")
val nameSeparator = " " val nameSeparator = " "
for (stat in result.options.stats) { for (stat in result.options.stats) {
Timber.d("analyse stat: $stat for report: $staticReport") // Timber.d("analyse stat: $stat for report: $staticReport")
// Get current performance // Get current performance
var query = performancesQuery(realm, staticReport, stat) 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 // Store if necessary, delete if necessary
val bestComputedResults = result.max(stat) val bestComputedResults = result.max(stat)
bestComputedResults?.let { computedResults -> bestComputedResults?.let { computedResults ->
messages.add("found new perf...")
val performanceQuery = computedResults.group.query val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator) val performanceName = performanceQuery.getName(this.context, nameSeparator)
@ -225,6 +237,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
var storePerf = true var storePerf = true
currentPerf?.let { currentPerf?.let {
messages.add("has current perf...")
currentPerf.name?.let { name -> currentPerf.name?.let { name ->
if (computedResults.group.query.getName(this.context, nameSeparator) == name) { if (computedResults.group.query.getName(this.context, nameSeparator) == name) {
storePerf = false storePerf = false
@ -246,6 +260,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
} }
} }
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) { if (currentPerf == null && storePerf) {
val performance = Performance( 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 } ?: 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 -> currentPerf?.let { perf ->
realm.executeTransaction { realm.executeTransaction {
Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}") // Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
perf.deleteFromRealm() 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 HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records 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 -> R.string.net_result
STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_ STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses

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

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

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

@ -4,27 +4,20 @@ import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.* 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.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow 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.RANDOM_PLAYER
import net.pokeranalytics.android.util.extensions.isSameDay
import net.pokeranalytics.android.util.extensions.mediumDate
import java.util.* 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 @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -44,13 +37,6 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
@Ignore @Ignore
override val viewType: Int = RowViewType.ROW_PLAYER.ordinal 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 { override fun isValidForDelete(realm: Realm): Boolean {
//TODO //TODO
return true return true
@ -69,70 +55,16 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
return R.string.relationship_error return R.string.relationship_error
} }
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.name 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) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
PlayerPropertiesRow.NAME -> this.name = value as String? ?: "" PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: "" PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerPropertiesRow.IMAGE -> this.picture = 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())
} }
return rows
} }
/** /**
@ -142,57 +74,6 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, StaticRow
return picture != null && picture?.isNotEmpty() == true 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 val initials: String
get() { get() {
return if (this.name.isNotEmpty()) { 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 val isPositive: Int
get() { get() {
return if (session?.isTournament() == true) { return if (session?.isTournament() == true) {
if (this.cashout ?: -1.0 >= 0.0) 1 else 0 // if cashout is null we want to count a negative session if ((this.cashout ?: -1.0) > 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else { } else {
if (this.net >= 0.0) 1 else 0 if (this.net >= 0.0) 1 else 0
} }

@ -676,42 +676,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
} }
fun getFormattedStakes(): String { fun getFormattedStakes(): String {
return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { NULL_TEXT } 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 // LifeCycle
/** /**
@ -719,8 +686,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
*/ */
fun delete() { fun delete() {
CrashLogging.log("Deletes session. Id = ${this.id}")
if (isValid) { if (isValid) {
// CrashLogging.log("Deletes session. Id = ${this.id}")
realm.executeTransaction { realm.executeTransaction {
cleanup() cleanup()
deleteFromRealm() deleteFromRealm()
@ -776,32 +743,12 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
when (row) { when (row) {
SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll? SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
SessionPropertiesRow.STAKES -> if (value is Stakes) { SessionPropertiesRow.STAKES -> if (value is Stakes) {
if (value.ante != null) { if (value.ante != null) {
this.cgAnte = value.ante this.cgAnte = value.ante
} }
if (value.blinds != null) { if (value.blinds != null) {
this.cgBlinds = value.blinds 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) { } else if (value == null) {
this.cgBlinds = null this.cgBlinds = null
this.cgAnte = null this.cgAnte = null
@ -812,7 +759,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
SessionPropertiesRow.BUY_IN -> { SessionPropertiesRow.BUY_IN -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.buyin = value as Double? localResult.buyin = value as Double?
// this.updateRowRepresentation()
} }
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> { SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
@ -951,7 +897,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.bbNet, this.bbNet,
this.estimatedHands 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_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
Stat.HANDS_PLAYED -> this.estimatedHands Stat.HANDS_PLAYED -> this.estimatedHands
@ -1086,33 +1032,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.result?.netResult = null 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? { private fun cleanupBlinds(blinds: String?): String? {
if (blinds == null) { if (blinds == null) {

@ -31,7 +31,8 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
BONUS(2, true), BONUS(2, true),
STACKING_INCOMING(3, true), STACKING_INCOMING(3, true),
STACKING_OUTGOING(4, false), 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> { companion object : IntSearchable<Value> {
@ -49,6 +50,7 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
STACKING_INCOMING -> R.string.stacking_incoming STACKING_INCOMING -> R.string.stacking_incoming
STACKING_OUTGOING -> R.string.stacking_outgoing STACKING_OUTGOING -> R.string.stacking_outgoing
TRANSFER -> R.string.transfer 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!") 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 { fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst() val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst()
return if (type != null) { return if (type != null) {

@ -190,9 +190,11 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/*** /***
* Configures a hand history with a [handSetup] * Configures a hand history with a [handSetup]
*/ */
fun configure(handSetup: HandSetup) { fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
this.playerSetups.removeAll(this.playerSetups) if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it } handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it } handSetup.ante?.let { this.ante = it }
@ -477,7 +479,12 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean? val heroWins: Boolean?
get() { get() {
return this.heroIndex?.let { heroIndex -> return this.heroIndex?.let { heroIndex ->
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 { } ?: run {
null null
} }
@ -639,19 +646,22 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pots.forEach { pot -> pots.forEach { pot ->
val winningPositions = compareHands(pot.positions.toList()) if (pot.positions.size > 1) { // we only consider contested pots
// Distributes the pot for each winners val winningPositions = compareHands(pot.positions.toList())
val share = pot.amount / winningPositions.size
winningPositions.forEach { p -> // Distributes the pot for each winners
val wp = wonPots[p] val share = pot.amount / winningPositions.size
if (wp == null) { winningPositions.forEach { p ->
val wonPot = WonPot() val wp = wonPots[p]
wonPot.position = p if (wp == null) {
wonPot.amount = share val wonPot = WonPot()
wonPots[p] = wonPot wonPot.position = p
} else { wonPot.amount = share
wp.amount += share wonPots[p] = wonPot
} else {
wp.amount += share
}
} }
} }
@ -733,7 +743,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return boardHasWildCard || playerCardHasWildCard return boardHasWildCard || playerCardHasWildCard
} }
val allFullCards: List<Card> private val allFullCards: List<Card>
get() { get() {
val cards = mutableListOf<Card>() val cards = mutableListOf<Card>()
cards.addAll(this.board) cards.addAll(this.board)
@ -753,4 +763,13 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return cards.filter { it.isSuitWildCard } 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) { fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) {
values.forEach { value -> values.forEach { value ->
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll() if (value != TransactionType.Value.EXPENSE) {
if (existing.isEmpty()) {
val type = TransactionType() val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
type.name = value.localizedTitle(context) if (existing.isEmpty()) {
type.additive = value.additive val type = TransactionType()
type.kind = value.uniqueIdentifier type.name = value.localizedTitle(context)
type.lock = true type.additive = value.additive
realm.insertOrUpdate(type) 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.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.BackupTask
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.findAll import net.pokeranalytics.android.util.extensions.findAll
import net.pokeranalytics.android.util.extensions.isSameMonth import net.pokeranalytics.android.util.extensions.isSameMonth
import java.util.* import java.util.*
@ -76,6 +78,7 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
AppGuard.requestPurchasesUpdate() AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed() this.homePagerAdapter?.activityResumed()
lookForCalendarBadge() lookForCalendarBadge()
checkForFailedBackups()
} }
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
@ -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.ui.fragment.ImportFragment
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.extensions.count import net.pokeranalytics.android.util.extensions.count
import timber.log.Timber
class ImportActivity : BaseActivity() { class ImportActivity : BaseActivity() {
@ -64,11 +63,13 @@ class ImportActivity : BaseActivity() {
val fragmentTransaction = supportFragmentManager.beginTransaction() val fragmentTransaction = supportFragmentManager.beginTransaction()
val fragment = ImportFragment() val fragment = ImportFragment()
val fis = contentResolver.openInputStream(fileURI) fragment.setData(fileURI)
Timber.d("Load fragment data with: $fis")
fis?.let { // val fis = contentResolver.openInputStream(fileURI)
fragment.setData(it) // Timber.d("Load fragment data with: $fis")
} // fis?.let {
// fragment.setData(it)
// }
fragmentTransaction.add(R.id.container, fragment) fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit() fragmentTransaction.commit()

@ -3,6 +3,7 @@ package net.pokeranalytics.android.ui.activity.components
import android.Manifest.permission.ACCESS_FINE_LOCATION import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
import android.view.MenuItem 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.CrashLogging
import net.pokeranalytics.android.util.LocationManager import net.pokeranalytics.android.util.LocationManager
import net.pokeranalytics.android.util.PermissionRequest import net.pokeranalytics.android.util.PermissionRequest
import net.pokeranalytics.android.util.Preferences
import java.util.*
class RootBottomSheetViewModel: ViewModel() { class RootBottomSheetViewModel: ViewModel() {
var rowRepresentable: RowRepresentable? = null var rowRepresentable: RowRepresentable? = null
@ -55,12 +58,14 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState") CrashLogging.log("$this.localClassName onCreate, savedInstanceState=$savedInstanceState")
setLanguage()
} }
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState) super.onCreate(savedInstanceState, persistentState)
CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState") CrashLogging.log("$this.localClassName onCreate: bundle=$savedInstanceState, persistentState=$persistentState")
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // fixes crash
setLanguage()
} }
override fun onResume() { override fun onResume() {
@ -129,6 +134,26 @@ abstract class BaseActivity : AppCompatActivity() {
fragmentTransaction.commit() 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 * 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), IMPORT(900),
SUBSCRIPTION(901), SUBSCRIPTION(901),
CURRENCY(902), CURRENCY(902),
PERMISSION_WRITE_EXTERNAL_STORAGE(1000) PERMISSION_WRITE_EXTERNAL_STORAGE(1000),
CAMERA(1001)
} }
enum class ResultCode(var value: Int) { enum class ResultCode(var value: Int) {

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

@ -163,14 +163,26 @@ fun showEditTextAlertDialog(
builder.setMessage(it) 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) val editText = EditText(context)
editText.inputType = inputType
editTextText?.let { editTextText?.let {
editText.text = SpannableStringBuilder(it) editText.text = SpannableStringBuilder(it)
} }
editText.setTextColor(ContextCompat.getColor(context, R.color.white)) 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) { _, _ -> builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
positiveAction?.invoke(editText.text.toString()) positiveAction?.invoke(editText.text.toString())

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

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

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

@ -1,6 +1,8 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -8,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
@ -18,6 +21,8 @@ import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -26,6 +31,7 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -41,7 +47,7 @@ import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.Date
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) { data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
@ -182,10 +188,24 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
adapter = dataListAdapter adapter = dataListAdapter
} }
binding.addButton.setOnClickListener { binding.addButton.setOnClickListener {
ReportCreationActivity.newInstanceForResult(this, requireContext()) 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) this.paApplication?.reportWhistleBlower?.addListener(this)
} }
@ -199,7 +219,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
private fun updateRows() { private fun updateRows() {
this.adapterRows.clear() this.adapterRows.clear()
if (this.reportSetups.size > 0) { if (this.reportSetups.isNotEmpty()) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom)) adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups) adapterRows.addAll(this.reportSetups)
} }
@ -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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
@ -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.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.FileUtils
import net.pokeranalytics.android.util.Language
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.StopNotificationManager
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
@ -51,7 +59,7 @@ import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.Date
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener { class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -68,11 +76,12 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
return fragment return fragment
} }
val rowRepresentation: List<RowRepresentable> by lazy { // fun rowRepresentation(context: Context): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>() // val rows = ArrayList<RowRepresentable>()
rows.addAll(SettingsRow.getRows()) // val hasBackupEmail = Preferences.getBackupEmail(context)?.isNotBlank() ?: false
rows // rows.addAll(SettingsRow.getRows(hasBackupEmail))
} // return rows
// }
} }
@ -156,27 +165,27 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
private fun updateMainCurrency(currencyCode: String) { private fun updateMainCurrency(currencyCode: String) {
Preferences.getDefaultCurrency(requireContext())?.currencyCode?.let { mainCurrencyCode -> val mainCurrencyCode = UserDefaults.currency.currencyCode
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ -> if (mainCurrencyCode == currencyCode) {
hideLoader() 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" context?.let { context ->
showEditTextAlertDialog(requireContext(), InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL, showEditTextAlertDialog(context, InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL,
message = message, editTextText = apiRate?.toString()) { value -> message = message, editTextText = apiRate?.toString()) { value ->
value.toDoubleOrNull()?.let { rate -> value.toDoubleOrNull()?.let { rate ->
updateMainCurrency(currencyCode, rate) updateMainCurrency(currencyCode, rate)
} }
} }
} }
} }
} }
@ -199,7 +208,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
} }
override fun adapterRows(): List<RowRepresentable> { override fun adapterRows(): List<RowRepresentable> {
return rowRepresentation return SettingsRow.getRows()
} }
override fun charSequenceForRow( override fun charSequenceForRow(
@ -211,6 +220,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext()) SettingsRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext())
SettingsRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else "" SettingsRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else ""
SettingsRow.CURRENCY -> UserDefaults.currency.symbol SettingsRow.CURRENCY -> UserDefaults.currency.symbol
SettingsRow.BACKUP_EMAIL -> Preferences.getBackupEmail(requireContext()) ?: ""
else -> "" else -> ""
} }
} }
@ -220,6 +230,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext()) SettingsRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext())
SettingsRow.SHOULD_SHOW_BLOG_TIPS -> Preferences.shouldShowBlogTips(requireContext()) SettingsRow.SHOULD_SHOW_BLOG_TIPS -> Preferences.shouldShowBlogTips(requireContext())
SettingsRow.SHOW_INAPP_BADGES -> Preferences.showInAppBadges(requireContext()) SettingsRow.SHOW_INAPP_BADGES -> Preferences.showInAppBadges(requireContext())
SettingsRow.BACKUP_EMAIL -> !Preferences.hasBackupEmail(requireContext())
else -> false else -> false
} }
} }
@ -236,11 +247,13 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
this.openPlayStoreAccount() this.openPlayStoreAccount()
} }
} }
SettingsRow.LANGUAGE -> this.showLanguagePopup()
SettingsRow.RATE_APP -> showReviewManager() SettingsRow.RATE_APP -> showReviewManager()
SettingsRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact) SettingsRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact)
SettingsRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path) SettingsRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingsRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value) SettingsRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingsRow.DEALT_HANDS_PER_HOUR -> DealtHandsPerHourActivity.newInstance(requireContext()) SettingsRow.DEALT_HANDS_PER_HOUR -> DealtHandsPerHourActivity.newInstance(requireContext())
SettingsRow.BACKUP_EMAIL -> this.editBackupEmail()
SettingsRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport() SettingsRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport()
SettingsRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport() SettingsRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport()
SettingsRow.FOLLOW_US -> { 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() { private fun showReviewManager() {
val manager = ReviewManagerFactory.create(requireContext()) val manager = ReviewManagerFactory.create(requireContext())
@ -277,6 +324,8 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed // completed
} }
} else { } else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result. // There was some problem, continue regardless of the result.
} }
} }

@ -162,7 +162,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val async = GlobalScope.async { val async = GlobalScope.async {
val s = Date() val s = Date()
Timber.d(">>> start...") // Timber.d(">>> start...")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh() realm.refresh()
@ -174,7 +174,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds") // Timber.d(">>> ended in $duration seconds")
} }
async.await() async.await()
@ -190,8 +190,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/ */
private fun createSessionGroupsAndStartCompute(realm: Realm): Report { private fun createSessionGroupsAndStartCompute(realm: Realm): Report {
val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let { var filter: Filter? = null
if (it.filterableType == currentFilterable) { it } else { null } context?.let { context ->
this.currentFilter(context, realm)?.let { current ->
if (current.filterableType == currentFilterable) { filter = current }
}
} }
val allStats: List<Stat> = listOf( val allStats: List<Stat> = listOf(
@ -238,7 +241,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats) val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats)
Timber.d(">>>>> Start computations...") // Timber.d(">>>>> Start computations...")
val options = Calculator.Options() val options = Calculator.Options()
val computedStats = mutableListOf<Stat>() 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.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.time.Period import java.time.Period
@ -75,7 +76,7 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
AppGuard.registerListener(this) AppGuard.registerListener(this)
if (!isNetworkAvailable()) { if (!requireContext().isNetworkAvailable()) {
Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show()
return return
} }
@ -158,10 +159,10 @@ class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, Pur
purchase.isEnabled = true purchase.isEnabled = true
purchase.setOnClickListener { purchase.setOnClickListener {
val network = isNetworkAvailable() val network = requireContext().isNetworkAvailable()
Timber.d("isNetworkAvailable = $network ") Timber.d("isNetworkAvailable = $network ")
if (!isNetworkAvailable()) { if (!network) {
Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.connection_unavailable, Toast.LENGTH_LONG).show()
return@setOnClickListener return@setOnClickListener
} }

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

@ -1,11 +1,10 @@
package net.pokeranalytics.android.ui.fragment.components 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.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import net.pokeranalytics.android.PokerAnalyticsApplication import net.pokeranalytics.android.PokerAnalyticsApplication
@ -42,6 +41,12 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
v.setPadding(0, statusBarHeight, 0, 0)
insets
}
} }
override fun onResume() { override fun onResume() {
@ -176,15 +181,6 @@ abstract class BaseFragment : Fragment() {
alternativeLabels) 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) { fun showSnackBar(message: String) {
this.view?.let { view -> this.view?.let { view ->
val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE) val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE)

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

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

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

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

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

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

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

@ -85,6 +85,8 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
onRowValueChanged(currencyCode, BankrollPropertiesRow.CURRENCY) onRowValueChanged(currencyCode, BankrollPropertiesRow.CURRENCY)
if (shouldShowCurrencyRate) { if (shouldShowCurrencyRate) {
refreshRate() 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.model.LiveData
import net.pokeranalytics.android.ui.activity.components.MediaActivity import net.pokeranalytics.android.ui.activity.components.MediaActivity
import java.io.File import java.io.File
import java.util.*
class EditableDataActivity : MediaActivity() { class EditableDataActivity : MediaActivity() {
@ -52,6 +51,11 @@ class EditableDataActivity : MediaActivity() {
initUI() initUI()
} }
// override fun onPause() {
// super.onPause()
// this.paApplication.backupOperator?.backupIfNecessary()
// }
/** /**
* Init UI * Init UI
*/ */

@ -9,6 +9,9 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -18,23 +21,35 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentPlayerBinding import net.pokeranalytics.android.databinding.FragmentPlayerBinding
import net.pokeranalytics.android.model.realm.Comment import net.pokeranalytics.android.model.realm.Comment
import net.pokeranalytics.android.model.realm.Player 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.ColorPickerActivity
import net.pokeranalytics.android.ui.activity.components.CameraActivity
import net.pokeranalytics.android.ui.activity.components.MediaActivity 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.RowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.showAlertDialog 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.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import timber.log.Timber
import java.io.File import java.io.File
/** /**
* Player data fragment * Player data fragment
*/ */
class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { 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 { companion object {
const val REQUEST_CODE_PICK_COLOR = 1000 const val REQUEST_CODE_PICK_COLOR = 1000
} }
@ -64,6 +79,13 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
player.color = if (color != Color.TRANSPARENT) color else null player.color = if (color != Color.TRANSPARENT) color else null
rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -76,23 +98,38 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
_binding = null _binding = null
} }
var pickVisualMediaRequest: ActivityResultLauncher<PickVisualMediaRequest>? = null
/** /**
* Init UI * Init UI
*/ */
private fun initUI() { private fun initUI() {
mediaActivity = parentActivity as MediaActivity? mediaActivity = parentActivity as MediaActivity?
player.updateRowRepresentation() this.playerModel.updateRowRepresentation()
if (!deleteButtonShouldAppear) { if (!deleteButtonShouldAppear) {
onRowSelected(0, PlayerPropertiesRow.NAME) onRowSelected(0, PlayerPropertiesRow.NAME)
} }
binding.addComment.setOnClickListener { binding.addComment.setOnClickListener {
val comment = player.addComment() val comment = this.playerModel.addComment()
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
onRowSelected(-1, comment) 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>) { override fun getPhotos(files: ArrayList<File>) {
@ -107,8 +144,8 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
return this return this
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable> {
return player.adapterRows() return this.playerModel.adapterRows()
} }
override fun viewTypeForPosition(position: Int): Int { override fun viewTypeForPosition(position: Int): Int {
@ -131,8 +168,8 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
tag: Int tag: Int
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
PlayerPropertiesRow.NAME -> if (player.name.isNotEmpty()) player.name else NULL_TEXT PlayerPropertiesRow.NAME -> player.name.ifEmpty { NULL_TEXT }
PlayerPropertiesRow.SUMMARY -> if (player.summary.isNotEmpty()) player.summary else NULL_TEXT PlayerPropertiesRow.SUMMARY -> player.summary.ifEmpty { NULL_TEXT }
else -> super.charSequenceForRow(row, context, 0) else -> super.charSequenceForRow(row, context, 0)
} }
} }
@ -144,6 +181,9 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
val data = arrayListOf(RowRepresentableEditDescriptor(row.content)) val data = arrayListOf(RowRepresentableEditDescriptor(row.content))
showBottomSheet(row, this, data, isClearable = false, isDeletable = true) showBottomSheet(row, this, data, isClearable = false, isDeletable = true)
} }
is HandHistory -> {
HandHistoryActivity.newInstance(this, row.id)
}
else -> super.onRowSelected(position, row, tag) else -> super.onRowSelected(position, row, tag)
} }
} }
@ -152,14 +192,19 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
when (row) { when (row) {
is Comment -> { is Comment -> {
row.updateValue(value, row) row.updateValue(value, row)
player.updateRowRepresentation() this.playerModel.updateRowRepresentation()
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
} }
else -> { else -> {
super.onRowValueChanged(value, row) super.onRowValueChanged(value, row)
if (row == PlayerPropertiesRow.NAME) { when (row) {
rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE) 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) { GlobalScope.launch(Dispatchers.Main) {
delay(300) delay(300)
showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = { showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = {
player.deleteComment(row) playerModel.deleteComment(row)
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
}) })
} }
@ -202,8 +247,15 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
builder.setItems(placesArray.toTypedArray()) { _, which -> builder.setItems(placesArray.toTypedArray()) { _, which ->
when (placesArray[which]) { when (placesArray[which]) {
getString(R.string.take_a_picture) -> mediaActivity?.openImageCaptureIntent(false) getString(R.string.take_a_picture) -> {
getString(R.string.library) -> mediaActivity?.openImageGalleryIntent(false) 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) -> { getString(R.string.select_a_color) -> {
ColorPickerActivity.newInstanceForResult(this, REQUEST_CODE_PICK_COLOR) ColorPickerActivity.newInstanceForResult(this, REQUEST_CODE_PICK_COLOR)
} }
@ -218,7 +270,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
override fun onDataSaved() { override fun onDataSaved() {
super.onDataSaved() super.onDataSaved()
player.cleanupComments() this.playerModel.cleanupComments()
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { 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.app.ActivityOptionsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.BadgeUtils
@ -254,10 +255,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
binding.messageBox.isVisible = false binding.messageBox.isVisible = false
} }
val viewManager = SmoothScrollLinearLayoutManager(requireContext()) // val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.menuRecyclerView.apply { binding.menuRecyclerView.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = viewManager layoutManager = LinearLayoutManager(requireContext())
} }
// Add button // Add button
@ -582,38 +583,38 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
private fun retrieveLatestBlogPosts() { private fun retrieveLatestBlogPosts() {
val now = Date().time this.context?.let { context ->
// if (true) { val now = Date().time
if (Preferences.shouldShowBlogTips(requireContext()) && Preferences.getLastBlogTipsRetrievalDate(requireContext()) + 24 * 3600 * 1000 < now) { if (Preferences.shouldShowBlogTips(context) && Preferences.getLastBlogTipsRetrievalDate(context) + 24 * 3600 * 1000 < now) {
BlogPostApi.getLatestPosts(requireContext()) { posts -> BlogPostApi.getLatestPosts(context) { posts ->
context?.let { Preferences.setLastBlogTipsRetrievalDate(now, context)
Preferences.setLastBlogTipsRetrievalDate(now, requireContext())
var count = 0 var count = 0
if (posts.isNotEmpty()) { if (posts.isNotEmpty()) {
Preferences.setLatestRetrievedBlogPostId(posts.first().id, requireContext()) Preferences.setLatestRetrievedBlogPostId(posts.first().id, context)
val id = Preferences.getLatestDisplayedBlogPostId(context)
val id = Preferences.getLatestDisplayedBlogPostId(requireContext())
count = posts.count { it.id > id } count = posts.count { it.id > id }
} }
displayBlogPostButton(count) displayBlogPostButton(count)
} }
} }
} }
} }
private fun displayBlogPostButton(newCount: Int = 0) { private fun displayBlogPostButton(newCount: Int = 0) {
var show = false
if (Preferences.shouldShowBlogTips(requireContext()) && newCount > 0) { context?.let { context ->
show = true var show = false
this.badgeDrawable?.number = newCount 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() { private fun showMenu() {
val menuContainer = binding.menuContainer 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 if (menuContainer.isAttachedToWindow) {
anim.start() 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.duration = 150
anim.addListener(object : AnimatorListenerAdapter() { anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation) super.onAnimationEnd(animation)
menuContainer.visibility = View.INVISIBLE menuContainer.visibility = View.INVISIBLE
finish() finish()
} }
// override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
// super.onAnimationEnd(animation, isReverse)
// menuContainer.visibility = View.INVISIBLE
// finish()
// }
}) })
anim.start() anim.start()

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

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

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

@ -916,7 +916,7 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
this.handSetup.setStraddlePositions(this.firstStraddlePosition!!, positions) this.handSetup.setStraddlePositions(this.firstStraddlePosition!!, positions)
} }
this.handHistory.configure(this.handSetup) // restart initial setup this.handHistory.configure(this.handSetup, true) // restart initial setup
this.sortedActions.load(this.handHistory) // recreate the sorted Actions this.sortedActions.load(this.handHistory) // recreate the sorted Actions
this.createRowRepresentation() // make the table rows this.createRowRepresentation() // make the table rows
@ -1058,6 +1058,7 @@ class EditorViewModel : ViewModel(), RowRepresentableDataSource, CardCentralizer
} }
fun toggleSettingsRows() { fun toggleSettingsRows() {
this.selectionLiveData.value = null
this.settingsExpanded = !this.settingsExpanded this.settingsExpanded = !this.settingsExpanded
this.createRowRepresentation() this.createRowRepresentation()
} }

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

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

@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.RectF import android.graphics.RectF
import kotlinx.coroutines.android.awaitFrame
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Action 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 * 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. * auto-replay videos, and we want viewers to visualize the end.
*/ */
private val visualOccurences: Int private val visualOccurrences: Int
get() { get() {
val step = this.currentStep val step = this.currentStep
return when { 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 * Generates images and image descriptor to build the video using ffmpeg
* Command line: https://trac.ffmpeg.org/wiki/Slideshow * Command line: https://trac.ffmpeg.org/wiki/Slideshow
*/ */
suspend fun generateVideoContent(context: Context): File { fun generateVideoContent(context: Context): File {
var ffmpegImageDescriptor = "" var ffmpegImageDescriptor = ""
var vo = 0.0 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 bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) 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) this.drawer.drawTable(canvas, context)
@ -550,7 +549,7 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
count++ count++
awaitFrame() // awaitFrame()
} }
nextStep() 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}") // 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 bitmap = Bitmap.createBitmap(this.width.toInt(), this.height.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) 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) this.drawer.drawTable(canvas, context)
frameHandler(bitmap, vo) frameHandler(bitmap, vo)
bitmap.recycle()
} }

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

@ -1,7 +1,10 @@
package net.pokeranalytics.android.ui.modules.handhistory.replayer package net.pokeranalytics.android.ui.modules.handhistory.replayer
import android.content.Context 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.content.res.ResourcesCompat
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
@ -250,7 +253,7 @@ class TableDrawer {
cardRects.forEachIndexed { j, cardRect -> cardRects.forEachIndexed { j, cardRect ->
val showVillainHands = Preferences.getShowVillainCards(context) 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 val card = cards?.get(j)!! // tested line before
drawCard(card, cardRect, canvas, context) drawCard(card, cardRect, canvas, context)
} else { // show hidden cards } 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.GridSpacingItemDecoration
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.noGroupingFormatted
import timber.log.Timber import timber.log.Timber
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
@ -156,8 +155,8 @@ class KeyboardAmountView : AbstractKeyboardView,
this.setInputConnection(editText) this.setInputConnection(editText)
editText.setText(amount?.noGroupingFormatted) // editText.setText(amount?.noGroupingFormatted)
editText.requestFocus() // editText.requestFocus()
} }

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

@ -11,6 +11,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async 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.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity 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.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.CrashLogging import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
@ -76,7 +80,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun onResume() { override fun onResume() {
super.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() this.refreshTimer()
} }
@ -126,10 +130,10 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
val viewManager = SmoothScrollLinearLayoutManager(requireContext()) // val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.recyclerView.apply { binding.recyclerView.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = viewManager layoutManager = LinearLayoutManager(requireContext())
} }
binding.floatingActionButton.setOnClickListener { binding.floatingActionButton.setOnClickListener {

@ -4,7 +4,6 @@ import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import kotlinx.android.synthetic.main.fragment_dealt_hands_config.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentDealtHandsConfigBinding import net.pokeranalytics.android.databinding.FragmentDealtHandsConfigBinding
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.ComputableResult
@ -50,8 +49,8 @@ class DealtHandsPerHourFragment : RealmFragment() {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
val userConfig = UserConfig.getConfiguration(this.getRealm()) val userConfig = UserConfig.getConfiguration(this.getRealm())
this.liveValue.hint = "${userConfig.liveDealtHandsPerHour}" this.binding.liveValue.hint = "${userConfig.liveDealtHandsPerHour}"
this.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}" this.binding.onlineValue.hint = "${userConfig.onlineDealtHandsPerHour}"
} }
@ -60,10 +59,10 @@ class DealtHandsPerHourFragment : RealmFragment() {
getRealm().executeTransaction { realm -> getRealm().executeTransaction { realm ->
val userConfig = UserConfig.getConfiguration(realm) val userConfig = UserConfig.getConfiguration(realm)
this.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour -> this.binding.liveValue.text.toString().toIntOrNull()?.let { liveDealtHandsPerHour ->
userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour userConfig.liveDealtHandsPerHour = liveDealtHandsPerHour
} }
this.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour -> this.binding.onlineValue.text.toString().toIntOrNull()?.let { onlineDealtHandsPerHour ->
userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour userConfig.onlineDealtHandsPerHour = onlineDealtHandsPerHour
} }
realm.copyToRealmOrUpdate(userConfig) realm.copyToRealmOrUpdate(userConfig)
@ -76,13 +75,15 @@ class DealtHandsPerHourFragment : RealmFragment() {
} }
} }
this.liveValue.clearFocus() this.binding.liveValue.clearFocus()
this.onlineValue.clearFocus() this.binding.onlineValue.clearFocus()
// Hides keyboard // Hides keyboard
val imm: InputMethodManager = val imm: InputMethodManager =
requireContext().getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager requireContext().getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view!!.windowToken, 0) imm.hideSoftInputFromWindow(view!!.windowToken, 0)
this.activity?.finish()
} }
} }

@ -27,14 +27,6 @@ data class DefaultLegendValues(
*/ */
open class LegendView : FrameLayout { 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 protected lateinit var legendLayout: ConstraintLayout
/** /**

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

@ -17,7 +17,7 @@ import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.* import com.github.mikephil.charting.data.*
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import 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.R
import net.pokeranalytics.android.calculus.ComputedStat import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
@ -96,6 +96,7 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
HAND_HISTORY(R.layout.row_hand_history_view), HAND_HISTORY(R.layout.row_hand_history_view),
CALENDAR_GRID_CELL(R.layout.cell_calendar_grid), CALENDAR_GRID_CELL(R.layout.cell_calendar_grid),
CALENDAR_TIME_UNIT_CELL(R.layout.cell_calendar_time_unit), 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_ACTION(R.layout.row_hand_action),
// ROW_HAND_STREET(R.layout.row_hand_cards), // 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_GRID_CELL -> CalendarGridCellHolder(layout)
CALENDAR_TIME_UNIT_CELL -> CalendarTimeUnitCellHolder(layout) CALENDAR_TIME_UNIT_CELL -> CalendarTimeUnitCellHolder(layout)
ROW_TAB -> RowTabViewHolder(layout)
// Separator // Separator
SEPARATOR -> SeparatorViewHolder(layout) 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 * 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) { override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
if (row is CellResult) { if (row is CellResult) {
itemView.timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background) val timeUnit = itemView.findViewById<View>(R.id.timeUnit)
timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background)
} }
} }
@ -675,17 +702,22 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
BindableHolder { BindableHolder {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { 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 { val listener = View.OnClickListener {
it.text = row.localizedTitle(itemView.context) adapter.delegate?.onRowSelected(position, row)
} }
itemView.findViewById<AppCompatTextView>(R.id.value)?.let { itemView.setOnClickListener(listener)
it.text = adapter.dataSource.charSequenceForRow(row, itemView.context)
} if (row is PerformanceRow) {
itemView.findViewById<AppCompatImageView>(R.id.badge)?.let {
it.isVisible = adapter.dataSource.boolForRow(row)
}
itemView.findViewById<AppCompatImageView>(R.id.nextArrow)?.let { itemView.findViewById<AppCompatImageView>(R.id.nextArrow)?.let {
it.visibility = if (row.report.hasGraph) { it.visibility = if (row.report.hasGraph) {
View.VISIBLE View.VISIBLE
@ -693,11 +725,6 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
View.GONE 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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnection
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import kotlinx.android.synthetic.main.view_keyboard_stakes.view.* import net.pokeranalytics.android.databinding.ViewKeyboardStakesBinding
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.util.BLIND_SEPARATOR import net.pokeranalytics.android.util.BLIND_SEPARATOR
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
@ -16,6 +16,9 @@ class StakesKeyboardView : LinearLayoutCompat {
var inputConnection: InputConnection? = null var inputConnection: InputConnection? = null
private var _binding: ViewKeyboardStakesBinding? = null
private val binding get() = _binding!!
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
init(context, null) init(context, null)
} }
@ -30,42 +33,51 @@ class StakesKeyboardView : LinearLayoutCompat {
private fun init(context: Context, attrs: AttributeSet?) { private fun init(context: Context, attrs: AttributeSet?) {
val layoutInflater = LayoutInflater.from(context) val layoutInflater = LayoutInflater.from(context)
val view = layoutInflater.inflate(R.layout.view_keyboard_stakes, this, false) // val view = layoutInflater.inflate(R.layout.view_keyboard_stakes, this, false)
view.value_0.text = "0" _binding = ViewKeyboardStakesBinding.inflate(layoutInflater, this, true)
view.value_1.text = "1"
view.value_2.text = "2" binding.value0.text = "0"
view.value_3.text = "3" binding.value1.text = "1"
view.value_4.text = "4" binding.value2.text = "2"
view.value_5.text = "5" binding.value3.text = "3"
view.value_6.text = "6" binding.value4.text = "4"
view.value_7.text = "7" binding.value5.text = "5"
view.value_8.text = "8" binding.value6.text = "6"
view.value_9.text = "9" binding.value7.text = "7"
view.value_decimal.text = DecimalFormatSymbols.getInstance().decimalSeparator.toString() binding.value8.text = "8"
view.value_back.text = "" binding.value9.text = "9"
view.value_separator.text = "/" binding.valueDecimal.text = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
binding.valueBack.text = ""
view.value_0.setOnClickListener { this.commitText("0") } binding.valueSeparator.text = "/"
view.value_1.setOnClickListener { this.commitText("1") }
view.value_2.setOnClickListener { this.commitText("2") } binding.value0.setOnClickListener { this.commitText("0") }
view.value_3.setOnClickListener { this.commitText("3") } binding.value1.setOnClickListener { this.commitText("1") }
view.value_4.setOnClickListener { this.commitText("4") } binding.value2.setOnClickListener { this.commitText("2") }
view.value_5.setOnClickListener { this.commitText("5") } binding.value3.setOnClickListener { this.commitText("3") }
view.value_6.setOnClickListener { this.commitText("6") } binding.value4.setOnClickListener { this.commitText("4") }
view.value_7.setOnClickListener { this.commitText("7") } binding.value5.setOnClickListener { this.commitText("5") }
view.value_8.setOnClickListener { this.commitText("8") } binding.value6.setOnClickListener { this.commitText("6") }
view.value_9.setOnClickListener { this.commitText("9") } binding.value7.setOnClickListener { this.commitText("7") }
view.value_decimal.setOnClickListener { this.commitText(DecimalFormatSymbols.getInstance().decimalSeparator.toString()) } binding.value8.setOnClickListener { this.commitText("8") }
view.value_separator.setOnClickListener { this.commitText(BLIND_SEPARATOR) } binding.value9.setOnClickListener { this.commitText("9") }
view.value_back.setOnClickListener { this.deleteText() } binding.valueDecimal.setOnClickListener { this.commitText(DecimalFormatSymbols.getInstance().decimalSeparator.toString()) }
binding.valueSeparator.setOnClickListener { this.commitText(BLIND_SEPARATOR) }
binding.valueBack.setOnClickListener { this.deleteText() }
val layoutParams = FrameLayout.LayoutParams( val layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT FrameLayout.LayoutParams.WRAP_CONTENT
) )
addView(view, layoutParams) // addView(binding, layoutParams)
// addView(view, layoutParams)
}
fun setSeparatorVisibility(visible: Boolean) {
binding.valueSeparator.visibility = if (visible) View.VISIBLE else View.GONE
} }
private fun commitText(string: String) { private fun commitText(string: String) {

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

@ -12,12 +12,13 @@ import net.pokeranalytics.android.ui.view.RowViewType
enum class PlayerPropertiesRow : RowRepresentable { enum class PlayerPropertiesRow : RowRepresentable {
IMAGE, IMAGE,
NAME, NAME,
SUMMARY; SUMMARY,
TAB_SELECTOR;
override val resId: Int? override val resId: Int?
get() { get() {
return when (this) { return when (this) {
IMAGE -> null IMAGE, TAB_SELECTOR -> null
NAME -> R.string.name NAME -> R.string.name
SUMMARY -> R.string.summary SUMMARY -> R.string.summary
} }
@ -29,13 +30,14 @@ enum class PlayerPropertiesRow : RowRepresentable {
IMAGE -> RowViewType.ROW_PLAYER_IMAGE.ordinal IMAGE -> RowViewType.ROW_PLAYER_IMAGE.ordinal
NAME -> RowViewType.TITLE_SUBTITLE.ordinal NAME -> RowViewType.TITLE_SUBTITLE.ordinal
SUMMARY -> RowViewType.TITLE_SUBTITLE.ordinal SUMMARY -> RowViewType.TITLE_SUBTITLE.ordinal
TAB_SELECTOR -> RowViewType.ROW_TAB.ordinal
} }
} }
override val bottomSheetType: BottomSheetType override val bottomSheetType: BottomSheetType
get() { get() {
return when (this) { return when (this) {
IMAGE -> BottomSheetType.NONE IMAGE, TAB_SELECTOR -> BottomSheetType.NONE
NAME -> BottomSheetType.EDIT_TEXT NAME -> BottomSheetType.EDIT_TEXT
SUMMARY -> BottomSheetType.EDIT_TEXT_MULTI_LINES SUMMARY -> BottomSheetType.EDIT_TEXT_MULTI_LINES
} }
@ -45,25 +47,4 @@ enum class PlayerPropertiesRow : RowRepresentable {
return null 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, CURRENCY,
DEALT_HANDS_PER_HOUR, DEALT_HANDS_PER_HOUR,
SHOW_INAPP_BADGES, SHOW_INAPP_BADGES,
BACKUP_EMAIL,
// Export // Export
EXPORT_CSV_SESSIONS, EXPORT_CSV_SESSIONS,
@ -80,7 +81,7 @@ enum class SettingsRow : RowRepresentable {
rows.addAll(arrayListOf(FOLLOW_US, DISCORD, BLOG_TIPS, SHOULD_SHOW_BLOG_TIPS)) rows.addAll(arrayListOf(FOLLOW_US, DISCORD, BLOG_TIPS, SHOULD_SHOW_BLOG_TIPS))
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.preferences)) 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.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.export))
rows.addAll(arrayListOf(EXPORT_CSV_SESSIONS, EXPORT_CSV_TRANSACTIONS)) rows.addAll(arrayListOf(EXPORT_CSV_SESSIONS, EXPORT_CSV_TRANSACTIONS))
@ -131,6 +132,7 @@ enum class SettingsRow : RowRepresentable {
DISCORD -> R.string.join_discord DISCORD -> R.string.join_discord
DEALT_HANDS_PER_HOUR -> R.string.dealt_hands_per_hour DEALT_HANDS_PER_HOUR -> R.string.dealt_hands_per_hour
SHOW_INAPP_BADGES -> R.string.show_inapp_badges SHOW_INAPP_BADGES -> R.string.show_inapp_badges
BACKUP_EMAIL -> R.string.backup_email
else -> null else -> null
} }
} }
@ -142,6 +144,7 @@ enum class SettingsRow : RowRepresentable {
BANKROLL_REPORT, TOP_10, PLAYERS -> RowViewType.TITLE_ICON_ARROW.ordinal BANKROLL_REPORT, TOP_10, PLAYERS -> RowViewType.TITLE_ICON_ARROW.ordinal
VERSION, SUBSCRIPTION -> RowViewType.TITLE_VALUE.ordinal VERSION, SUBSCRIPTION -> RowViewType.TITLE_VALUE.ordinal
LANGUAGE, CURRENCY -> RowViewType.TITLE_VALUE_ARROW.ordinal LANGUAGE, CURRENCY -> RowViewType.TITLE_VALUE_ARROW.ordinal
BACKUP_EMAIL -> RowViewType.TITLE_BADGE_VALUE.ordinal
FOLLOW_US -> RowViewType.ROW_FOLLOW_US.ordinal FOLLOW_US -> RowViewType.ROW_FOLLOW_US.ordinal
STOP_NOTIFICATION, SHOULD_SHOW_BLOG_TIPS, SHOW_INAPP_BADGES -> RowViewType.TITLE_SWITCH.ordinal STOP_NOTIFICATION, SHOULD_SHOW_BLOG_TIPS, SHOW_INAPP_BADGES -> RowViewType.TITLE_SWITCH.ordinal
STOP_NOTIFICATION_MESSAGE -> RowViewType.INFO.ordinal STOP_NOTIFICATION_MESSAGE -> RowViewType.INFO.ordinal

@ -16,7 +16,7 @@ import java.util.*
class BottomSheetViewModelFactory(var row: RowRepresentable, var delegate: RowRepresentableDelegate): ViewModelProvider.Factory { 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 BottomSheetViewModel(row) as T
// return modelClass.getConstructor(RowRepresentable::class.java).newInstance(row) // 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.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream
class FileUtils { class FileUtils {
@ -55,3 +56,18 @@ 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 package net.pokeranalytics.android.util
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.* import android.graphics.*
import android.graphics.Paint.FILTER_BITMAP_FLAG
import android.media.ExifInterface
import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@ -24,77 +15,6 @@ import java.util.*
object ImageUtils { 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) * Save a bitmap into a file (& apply 90% compression)
* *
@ -118,45 +38,6 @@ 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 * 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 var locationResultReceived = false
val locationCallback = object : LocationCallback() { val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult) super.onLocationResult(locationResult)
locationResult?.locations?.let { locationResult.locations.firstOrNull()?.let { location ->
if (!locationResultReceived && it.isNotEmpty()) { if (!locationResultReceived) {
locationResultReceived = true locationResultReceived = true
callback(it.first()) callback(location)
fusedLocationClient.removeLocationUpdates(this) fusedLocationClient.removeLocationUpdates(this)
} }
} }
@ -194,7 +195,12 @@ class LocationManager(private var context: Context) {
locationRequest.interval = 200L locationRequest.interval = 200L
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper()) Looper.myLooper()?.let { looper ->
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, looper)
} ?: run {
callback.invoke(null)
}
} else { } else {
// If we don't have the permission, return null // If we don't have the permission, return null
callback.invoke(null) callback.invoke(null)

@ -33,7 +33,7 @@ class Preferences {
PATCH_SESSION_SETS("patchSessionSet"), PATCH_SESSION_SETS("patchSessionSet"),
PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"), PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"),
// PATCH_BLINDS_FORMAT("patchBlindFormat"), // PATCH_BLINDS_FORMAT("patchBlindFormat"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions"), PATCH_COMPUTABLE_RESULTS("patchPositiveSessions_v2"),
PATCH_ZERO_TABLE("patchZeroTable"), PATCH_ZERO_TABLE("patchZeroTable"),
SHOW_STOP_NOTIFICATIONS("showStopNotifications"), SHOW_STOP_NOTIFICATIONS("showStopNotifications"),
ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"), ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"),
@ -49,7 +49,11 @@ class Preferences {
CLEAN_BLINDS_FILTERS("deleteBlindsFilters"), CLEAN_BLINDS_FILTERS("deleteBlindsFilters"),
SHOW_IN_APP_BADGES("showInAppBadges"), SHOW_IN_APP_BADGES("showInAppBadges"),
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"), LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"),
PATCH_RATED_AMOUNT("patchRatedAmount[new field]") PATCH_RATED_AMOUNT("patchRatedAmount[new field]"),
BACKUP_EMAIL("backupEmail"),
LANGUAGE_CODE("languageCode"),
SESSIONS_BACKUP_SUCCESS("sessionsBackupSuccess"),
TRANSACTIONS_BACKUP_SUCCESS("transactionsBackupSuccess")
} }
enum class FeedMessage { enum class FeedMessage {
@ -327,6 +331,44 @@ class Preferences {
setLong(Keys.LAST_CALENDAR_BADGE_DATE, date, context) 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 package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.deleteFromRealm import io.realm.kotlin.deleteFromRealm
import net.pokeranalytics.android.model.interfaces.Identifiable 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.model.realm.Session
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber
/** /**
* The various sources of CSV * The various sources of CSV
@ -18,7 +18,8 @@ enum class DataSource {
POKER_INCOME, POKER_INCOME,
POKER_BANKROLL_TRACKER, POKER_BANKROLL_TRACKER,
RUN_GOOD, RUN_GOOD,
POKER_AGENT; POKER_AGENT,
POKER_BASE;
val availableDateFormats: List<String> val availableDateFormats: List<String>
get() { 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>() 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 { data?.let {
// Timber.d(">>>>>>> identifier added: ${it.id}") // Timber.d(">>>>>>> identifier added: ${it.id}")
this.realmModelIds.add(it.objectIdentifier) 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 * 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) { 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 } 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 return count >= mandatoryFields.size
} }

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

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit 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 * 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 isTournament = isTournament ?: false
val session = Session.newInstance(realm, isTournament, managed = 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 stackingIn: Double? = null
var stackingOut: Double? = null var stackingOut: Double? = null
var expense: Double? = null
var sb: Double? = null var sb: Double? = null
var bb: Double? = null var bb: Double? = null
@ -216,6 +218,9 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
is SessionField.StackingOut -> { is SessionField.StackingOut -> {
stackingOut = field.parse(value) stackingOut = field.parse(value)
} }
is SessionField.Expense -> {
expense = field.parse(value)
}
is SessionField.ListCustomField -> { is SessionField.ListCustomField -> {
val entry = field.customField.getOrCreateEntry(realm, value) val entry = field.customField.getOrCreateEntry(realm, value)
session.customFieldEntries.add(entry) session.customFieldEntries.add(entry)
@ -275,6 +280,11 @@ abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!) val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!)
this.addAdditionallyCreatedIdentifiable(transaction) 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 return managedSession
} else { } else {

@ -20,7 +20,8 @@ class ProductCSVDescriptors {
pokerAnalytics6iOS, pokerAnalytics6iOS,
pokerAnalyticsAndroidSessions, pokerAnalyticsAndroidSessions,
pokerAnalyticsAndroid6Sessions, pokerAnalyticsAndroid6Sessions,
pokerAnalyticsAndroidTransactions pokerAnalyticsAndroidTransactions,
// pokerBase
) )
private val pokerAgent: CSVDescriptor 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 private val pokerAnalyticsiOS: SessionCSVDescriptor
get() { get() {
return SessionCSVDescriptor( return SessionCSVDescriptor(
@ -210,6 +226,7 @@ class ProductCSVDescriptors {
SessionField.LimitType("Limit"), SessionField.LimitType("Limit"),
SessionField.Game("Game"), SessionField.Game("Game"),
SessionField.TableSize("Table Size"), SessionField.TableSize("Table Size"),
SessionField.HandsCount("Hands Count"),
SessionField.Location("Location"), SessionField.Location("Location"),
SessionField.Bankroll("Bankroll"), SessionField.Bankroll("Bankroll"),
SessionField.CurrencyCode("Currency Code"), SessionField.CurrencyCode("Currency Code"),

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

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

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

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import android.content.Context
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Bankroll
@ -13,7 +14,7 @@ import java.util.*
class TransactionCSVDescriptor(source: DataSource, vararg elements: CSVField) : class TransactionCSVDescriptor(source: DataSource, vararg elements: CSVField) :
DataCSVDescriptor<Transaction>(source, *elements) { 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 date: Date? = null
var typeName: String? = 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView" android:id="@+id/nestedScrollView"

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

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

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

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

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