Compare commits

..

342 Commits
bs ... master

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
Laurent c90f4cd52f Ask for rate when changing main currency 3 years ago
Laurent 1f595bc399 Currency API update 3 years ago
Laurent db5716971a revert attempt to fix 3 years ago
Laurent 2b8464f75c Attempt to fix truncated day of week 3 years ago
Laurent 5a0d86e533 Fixes 3 years ago
Laurent d852d62c93 Adds transaction filters to the calendar tab 3 years ago
Laurent 5c26af304c Bumps to 6.0.2 3 years ago
Laurent aa2e144fb9 First working implementation of transaction filters 3 years ago
Laurent 08167f721a Refactoring and warning removal 3 years ago
Laurent 7c527699c2 Fixes migration 3 years ago
Laurent d097e96cf8 bumps code 3 years ago
Laurent 4e4f38f5b4 Adds translation 3 years ago
Laurent 46f8b3ae0d Fixes issue with multiple lines appearing 3 years ago
Laurent 6eac9137d7 Remove useless dependency 3 years ago
Laurent e0d3569311 Bumps version to 6.0 3 years ago
Laurent 1d77e929bf remove log 3 years ago
Laurent 991cb6bf6e Fixes 3 years ago
Laurent 80a617b5ce Launch reportwhistleblower if necessary 3 years ago
Laurent e0213e4a51 Adds empty screen text 3 years ago
Laurent 54467e34cc Improvement and fixes 3 years ago
Laurent 74f7e8d422 Fixes in QueryCondition operators 3 years ago
Laurent 0e27805a8d Little improvements 3 years ago
Laurent 45929e0ccb Fixes zero tables bug 3 years ago
Laurent be1819bab9 improve formatting 3 years ago
Laurent b66f67be23 Fixes issue when settings number of tables of 0 3 years ago
Laurent 89effc942f Fixes + calendar badge 3 years ago
Laurent f5115e53e4 Improvements 3 years ago
Laurent af1024c7bc Various fixes 3 years ago
Laurent 925ab12faf first commit 3 years ago
Laurent baf755c4c8 Adds bankroll transfer 3 years ago
Laurent a4ff88f099 Fixes CSV export stakes issue 3 years ago
Laurent 2db90d2da1 Fixes currency API 3 years ago
Laurent f690a7108a Fixes subscription not refreshed in settings when ending 3 years ago
Laurent 7c77bc9cd8 Fixes crash 3 years ago
Laurent 49296c265c Upgrade billing API from v3 to v5 3 years ago
Laurent a32b9afd06 Bumps to version 138 / 5.5.2 3 years ago
Laurent 50564a07b5 Fixes issue with duplicates 3 years ago
Laurent e0511985a3 Bumps to 5.5.1 / 137 3 years ago
Laurent 0526249a6d Fix crash due to remaining blind filters in databases 3 years ago
Laurent 33f04d9519 merge blinds branch 3 years ago
Laurent 222af39305 Bumps to 5.5 (136) 3 years ago
Laurent 19a364c23c Fixes cards formatting in text export 3 years ago
Laurent fb380937f1 Increase button size 3 years ago
Laurent f35db342b6 Fixes straddle setup with complex blind values 3 years ago
Laurent cb7711e33b Fixes issue with unappearing screen 3 years ago
Laurent e40c5c0cd6 Adds number of tables row for online cash game 3 years ago
Laurent 4a4943864b Fixes import issues 3 years ago
Laurent 0c7d99f288 Fixes keyboard issues 3 years ago
Laurent d2edbb0d3c Merge branch 'blinds' of gitlab.com:stax-river/poker-analytics into blinds 3 years ago
Laurent 041c3b5985 Various fixes 3 years ago
Razmig Sarkissian 2306dabddc fix issue with new cash game truncated in french 3 years ago
Laurent 2893f9cd19 Bumps to 5.4.11 / 135 3 years ago
Laurent 4fd5de50ab Fixes import issues 3 years ago
Laurent 84a5690024 Fixes focus issue 3 years ago
Laurent 788c6a597d Fixes crash with possible negative limit 3 years ago
Laurent 498c8a3a84 Fixes weird migration issue 3 years ago
Laurent 02fb44056a Fixes 3 years ago
Laurent 1201541dfe Fixes and improvements 3 years ago
Laurent 6b87d372e6 Fixes sorting isse 3 years ago
Laurent a5e7030c7c Fix test build 3 years ago
Laurent d61996f187 Fixes sorting issue 3 years ago
Laurent a34ec49f0d Sorting first draft 3 years ago
Laurent 8f7e25ee6b Stakes filter implementation 3 years ago
Laurent 687423fb3e Improvements 3 years ago
Laurent f48683d683 Complext blinds for hand histories 3 years ago
Laurent 546e7a5777 Fixes stakes formatting 3 years ago
Laurent 2b64b712ca Change color of background 3 years ago
Laurent b20bb4815d Complex blinds first commit 3 years ago
Laurent d095a8c5af Fixes import issues 3 years ago
Laurent 6783e4a26f Fixes focus issue 3 years ago
Laurent 0cea6586a3 bumps to 5.4.9 3 years ago
Laurent aa8112b965 Bumps version 4 years ago
Laurent 1ee6b2d142 First draft for review requests 4 years ago
Laurent dc0840461b Fixes bug where streets would not show up in text export 4 years ago
Laurent 6b334da9b3 Environment update 4 years ago
Laurent 0c2b5b0e10 Bumps version to 5.4.6 / 129 4 years ago
Laurent c43b48c0cb Added surrounding quotes for csv fields 4 years ago
Laurent 0a99742819 Update ffmpeg lib to fix occasional crash 4 years ago
Laurent 98d22219b0 Fixes issue with android 30 for taking / selecting pictures 4 years ago
Laurent fb4633c50d commit old editions 4 years ago
Laurent dbd0586d44 Puts un-impactful suits to suit wildcards 5 years ago
Laurent 4fd90a3811 Adds comment 5 years ago
Laurent 02ba62654f Fixes crash occuring when setting player setups 5 years ago
Laurent 2a49f831ea Adds confirmation popup when hand uses wildcards + fixes bug in ffmpeg export by adding option 5 years ago
Laurent 4a43d2fac3 Bumps version to 124 / 5.4.2 5 years ago
Laurent a87e18f950 Fixes issue on Android Q where the gif type was wrong 5 years ago
Laurent 55b15780d1 Adds pre Q and post Q hand history exports 5 years ago
Laurent f874f35ad9 Removes deprecated file management API 5 years ago
Laurent 21d700db1e Improves video rythm 5 years ago
Laurent e34345f869 Add note 5 years ago
Laurent 0890674f95 Fixes crash 5 years ago
Laurent 90adf28752 Upgrades realm plugin 5 years ago
Laurent 157ebe0c38 Adds tips to stats 5 years ago
Laurent fa7aad048c Updated realm gradle plugin 5 years ago
Laurent c19aca865c Adds ratedTips field to ComputableResult + changed isPositive for tournaments 5 years ago
Laurent 3a6a93b0be Bumps version to 123 / 5.4.1 5 years ago
Laurent eba3d1d5ac Slight crashlog improvement 5 years ago
Laurent d911a05359 Fixes crash due to unattached fragment 5 years ago
Laurent fb717e09bf Fixes crash occuring when the blog post request response arrives when the fragment does not exists 5 years ago
Laurent 3d5f53a41c Bumps version to 122 / 5.4 5 years ago
Laurent 0698d956ef Improves filters names 5 years ago
Laurent 2fe59caf4a Fixes recycler view position 5 years ago
Laurent da73975e6d Fixes issue with lack of data message 5 years ago
Laurent 4624bfad24 cleanup 5 years ago
Laurent b46a7a190c Merge branch 'master' of gitlab.com:stax-river/poker-analytics 5 years ago
Laurent 2d68256504 Fixes grid calendar item sorting 5 years ago
Razmig Sarkissian f15703c306 add fr loc 5 years ago
Laurent 2e3d00ce41 PBT import fix 5 years ago
Laurent 6c6cf9a60f Adds more logging when a crash occurs 5 years ago
Laurent 5f675d9f67 Adds logging when a crash occurs 5 years ago
Laurent eb86679fa7 Surrounds crash with try/catch block 5 years ago
Laurent 6325daf4c3 Fixes calendar cash/tournament not being used for All/Past x days 5 years ago
Laurent bb3aa51cee cleanup 5 years ago
Laurent 8e06721b1b Fixes badge glitch 5 years ago
Laurent 0ee4fc7708 Adds default NL Holdem game 5 years ago
Laurent 689cdd844a Fixes an issue where the Game and Location reports showed bad legend 5 years ago
Laurent 25e5119822 Block the possibility to give an empty report name 5 years ago
Laurent 53465356d9 Adds evolution value for ITM ratio 5 years ago
Laurent baa97a4641 Update? 5 years ago
Laurent ab3ad228dd Fixes spacing and dimension issues 5 years ago
Laurent 4044cee176 Grid calendar + hands count CSV management 5 years ago
Laurent 5f2ffdb306 remove comments 5 years ago
Laurent e9e262c3c0 Fixes calendar sliding row tapping + adds new class for grid calendar 5 years ago
Laurent 6aab1c3a3b Adds QueryCondition name fine tuning 5 years ago
Laurent f11b4c94ab Adds sliding month and year to Calendar 5 years ago
Laurent ef80ab83ee Adds ALL option to Calendar 5 years ago
Laurent e663e55f1f Adds hands count management 5 years ago
Laurent 6b7eb38fc9 Adds hands configuration initial commit 5 years ago
Laurent 8139c9d8b3 Adds ITM ratio stat 5 years ago
Laurent b05ba6f61d Added missing logic for posts retrieval 5 years ago
Laurent f1ee656e52 Fixes bad key use 5 years ago
Laurent ba5a9c74e3 Enable one day tips retrieval 5 years ago
Laurent b448dfaab7 Adds blog tips button 5 years ago
Laurent 4b1f3abc4e Bumps version to 5.1.6 5 years ago
Laurent 744af57a08 Fixes crash 5 years ago
Laurent 4252ae7360 Hope it fixes crash 5 years ago
Laurent 886c4095d5 Fixes a crash when setting straddle then changing the number of players 5 years ago
Laurent 03361961b5 Removes chatty log 5 years ago
Laurent 148eed3a95 Remove useless capitalizations 5 years ago
Laurent 0af3ad6fdd Update proguard rules for more deobfuscating 5 years ago
Laurent 772b304877 Adds crash logs to better understand how stopping a pending session happens 5 years ago
Laurent 1484ed2f74 Fixes crash with font loading 5 years ago
Laurent cb228b8fb1 Fixes possible crash when deleting a session 5 years ago
Laurent e8c298ade6 Put pro sub for debug 5 years ago
Laurent 033c204fd0 Package name refactoring 5 years ago
Laurent 1ea5e93a0a Bumps version 5 years ago
Laurent 0c38c52c40 Fixes crash 5 years ago
Laurent a6178d94aa Avoid unecessary crash 5 years ago
Laurent e9d2d3ab98 Version bump 5 years ago
Laurent cd22a60dcc Refactoring + crash fix 5 years ago
Laurent 4cc9a73120 Refactoring 5 years ago
Laurent ecfbb87a42 Might fix crash, at least give better logs 5 years ago
Laurent 5d5fe4c422 Fix warning 5 years ago
Laurent b51f065b1e Fix crash 5 years ago
Laurent b7d398b79b Better error for crash 5 years ago
Laurent 28f823b121 Crash fix attempt 5 years ago
Laurent 2f029d5818 Fixes crash with binding 5 years ago
Laurent ea624da182 CrashLogging moved 5 years ago
Laurent a928991ac3 Delete another unused class 5 years ago
Laurent 6c29b8e493 Delete unused class 5 years ago
Laurent d054ab04c3 Separate RowUpdatable from model + name refactoring 5 years ago
Laurent 54434ac955 Name Refactoring 5 years ago
Razmig Sarkissian 99d7fdfc8c add StatRepresentable and Color files back 5 years ago
Laurent 3e257e0905 More separation 5 years ago
Laurent 1b486e0ae7 Separate Report Display from Calculator.Options 5 years ago
Laurent feb7ba5c1b Bumps version 5 years ago
Laurent 423d06d5dd Remove Poker Rumble 5 years ago
Laurent 4568acc784 Deprecated api removed and improved UI 5 years ago
Laurent 306b9d80dc Warnings removal 5 years ago
Laurent 006520b52c Possibly fixes crash 5 years ago
Laurent c3cc059973 Improved code and exceptions 5 years ago
Laurent 0c7793aa54 Fixes double add of evolution values for bb per 100 5 years ago
Laurent 21ad16c4c3 Avoid selecting multiple stats for progress report 5 years ago
Laurent 86cf721bbd use Crashlogging shortcut instead of firebase.getinstance 5 years ago
Laurent b3694c737b Fixes issue where actions were not displayed 5 years ago
Laurent 24724ce64b Name refactoring 5 years ago
Razmig Sarkissian e0a2dc1c48 fix crash with BottomSheetMultiSelectionFragment 5 years ago
Laurent 1d4f5d367f Fixes issue where custom comparison did not show multi lines view 5 years ago
Laurent c460acd631 Fixes Custom Table report that was empty 5 years ago
Laurent 972c73500c Fixes crash 5 years ago
Razmig Sarkissian f83a834011 fix crash with calendar and report tab bar id 5 years ago
Laurent 0f44c9f06a Fixes issue where the number of session bar graph was not displayed initially 5 years ago
Laurent d377072158 Fixes crashes 5 years ago
Laurent 2652309e0b Fix warnings 5 years ago
Laurent 765fbf8d53 Fix crashes due to binding introduction 5 years ago
Laurent e05d0671e6 Use new view bindings 5 years ago
Laurent bc1c653bc2 gradle udpate 5 years ago
Laurent f4f42cabdf Bump version number to 116 5 years ago
Laurent b2a4757560 Fixes stackoverflow crash 5 years ago
Laurent da6a6c53ba Fixes crash when clearing custom field values 5 years ago
Laurent 50c22ea40e Firebase fix #2 5 years ago
Laurent 0d07c2e625 Firebase fix 5 years ago
Laurent 8a22be590a Update firebase crashltics 5 years ago
Laurent 3f79cceaa5 gradle version upgrade 5 years ago
Laurent 7e1a3de031 Clean code 5 years ago
Laurent 50d3287a24 Remove warnings 5 years ago
Laurent 5bbfea13a4 Upgrade billing / cleanup 5 years ago
Laurent 21d72247af Remove ability to deselect all result capture method 5 years ago
Laurent f67cc48cd4 Upgrade gradle for Android studio 4 5 years ago
Laurent 26cd33c2fc Upgrade crashlytics from fabric to firebase 5 years ago
Laurent 77aab66a81 Bumps version to 5.1 / 112 5 years ago
Laurent 79c41d33b3 Fixes #36 : crash with custom field filters 5 years ago
Laurent 5c683eb369 Fixes #37 : Reports header issue + cleanup 5 years ago
Laurent 56a10395cc Change style of popup 5 years ago
Laurent 81d8f5d8d9 Adds ability to change result capture method inside the session 5 years ago
Laurent 8dd74bf99e Improves most used filters layout + better hints 5 years ago
Laurent fca3490f63 hides Go pro button by default, shows if no subscription is received 5 years ago
Laurent da8d8c2be1 Fixes issues with result capture method 5 years ago
Laurent 777d4826ba Merge branch 'master' of gitlab.com:stax-river/poker-analytics 5 years ago
Laurent 141a0f49d7 Adds ability to change the way to enter results for online sessions 5 years ago
Laurent b2b36dfdcc Move the QueryCondition item wrapping from QueryCondition to the FilterSectionRow 5 years ago
Laurent 77888e1501 Remove bad code 5 years ago
Laurent eeaba2876a Fixes issue where GoPro button appears even if the user is subscribed 5 years ago
Laurent 6c2367aa7f Merge branch 'filterfix' 5 years ago
Laurent f9d3f57647 Adds october 2021 flavor 5 years ago
Laurent b65695d80c Put existing values as hints by default in bottom sheet 5 years ago
Laurent 5c4cfa1f4e Fix warnings 5 years ago
Laurent 7cd1117ce3 Cleans commented code 5 years ago
Laurent 6722ba2218 Fixes issue with multi values list loading 5 years ago
Laurent f38a85b48f Reinstate filter use 5 years ago
Laurent 8bdeb40824 Fixes a crash 5 years ago
Laurent c358d4131f Fixes various problems 5 years ago
Laurent b7613644c6 Fixes important bugs 5 years ago
Laurent 457ef13c2f More fixes and amazing improvements 5 years ago
Laurent fb0c5f7933 More fixes 5 years ago
Laurent 7924e19fe3 Changed QueryCondition single values from optional to mandatory + Details ViewModel creation and refactoring 5 years ago
Laurent 1322952b47 Fixes a lifecycle crash 5 years ago
Laurent deba27b724 Bumps code to 111 5 years ago
Laurent 7768ddeda4 Fix crashes due to Realm not being up to date 5 years ago
Laurent a1887e4c8f Bumps to 5.0.9 / 110 5 years ago
Laurent 6cf302a680 Fixes an API 29 crash with a workaround 5 years ago
Laurent 7612fad0ca Crash fixes attempts 5 years ago
Laurent 37c23c7094 Flavor added 5 years ago
Laurent 860d203be7 Remove unused rows 5 years ago
Laurent 19cf418688 Bumps versionCode to 109 5 years ago
Laurent beb1509388 Fixes warnings 5 years ago
Laurent 42a55cd3da Fixes layout issue for smaller screens 5 years ago
Laurent 6491a67d32 Adds Go Pro button at the bottom 5 years ago
Laurent 759b1397db Upgrade build.gradle and eliminate warnings 5 years ago
Laurent d1b2edbefe Remove warning 5 years ago
Laurent 79d429c3da Upgrade test libs version 5 years ago
Laurent 8ce1e6dd0e Removed logs 5 years ago
Laurent ec5e6b427e Upgrade billing API to 3.0 5 years ago
Laurent 6c36863185 Remove unused MMediaMuxer 5 years ago
Laurent f79f2f60cd Bumps version to 5.0.7 / 105 5 years ago
Laurent 7c3bc15971 Crash fixing attempts #2 5 years ago
Laurent 8ad0ae945e Crash fixing attempts 5 years ago
  1. 139
      CLAUDE.md
  2. 1
      app/april2021/release/output.json
  3. 114
      app/build.gradle
  4. 24
      app/google-services.json
  5. 25
      app/proguard-rules.pro
  6. 19
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/FavoriteSessionUnitTest.kt
  7. 8
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  8. 82
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt
  9. 39
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt
  10. 10
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt
  11. 80
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  12. 3
      app/src/debug/AndroidManifest.xml
  13. 134
      app/src/main/AndroidManifest.xml
  14. BIN
      app/src/main/ic_launcher-playstore.png
  15. 59
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  16. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  17. 49
      app/src/main/java/net/pokeranalytics/android/api/BlogPostApi.kt
  18. 60
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  19. 47
      app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt
  20. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  21. 10
      app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt
  22. 145
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  23. 111
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  24. 247
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  25. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  26. 42
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  27. 28
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  28. 10
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  29. 12
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/AggregationTypeExtensions.kt
  30. 84
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ComputedResultsExtensions.kt
  31. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportDisplay.kt
  32. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportExtensions.kt
  33. 81
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/StatRepresentable.kt
  34. 299
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  35. 25
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  36. 46
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  37. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  38. 13
      app/src/main/java/net/pokeranalytics/android/model/Stakes.kt
  39. 13
      app/src/main/java/net/pokeranalytics/android/model/blogpost/BlogPost.kt
  40. 27
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  41. 4
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  42. 51
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  43. 507
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  44. 80
      app/src/main/java/net/pokeranalytics/android/model/handhistory/BoardManager.kt
  45. 56
      app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt
  46. 14
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  47. 189
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  48. 2
      app/src/main/java/net/pokeranalytics/android/model/interfaces/UsageCountable.kt
  49. 168
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  50. 137
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  51. 85
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  52. 3
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  53. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  54. 97
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  55. 3
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  56. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  57. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  58. 15
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  59. 5
      app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt
  60. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  61. 138
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  62. 29
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  63. 18
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  64. 610
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  65. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  66. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  67. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt
  68. 94
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  69. 26
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  70. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  71. 58
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt
  72. 245
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  73. 14
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/BankrollRow.kt
  74. 57
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/CustomFieldRow.kt
  75. 11
      app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt
  76. 15
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  77. 31
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  78. 20
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  79. 48
      app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt
  80. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  81. 12
      app/src/main/java/net/pokeranalytics/android/ui/activity/GDPRActivity.kt
  82. 54
      app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt
  83. 175
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  84. 18
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  85. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/ProgressReportActivity.kt
  86. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt
  87. 96
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  88. 187
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt
  89. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt
  90. 128
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  91. 24
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/ReportActivity.kt
  92. 11
      app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt
  93. 55
      app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt
  94. 7
      app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt
  95. 5
      app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt
  96. 302
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  97. 24
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt
  98. 81
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  99. 106
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  100. 96
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -1 +0,0 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":52184,"versionName":"2.4.3_april2021","enabled":true,"outputFile":"PokerAnalytics_2.4.3(84)_200618_0952_release.apk","fullName":"april2021Release","baseName":"april2021-release","dirName":""},"path":"PokerAnalytics_2.4.3(84)_200618_0952_release.apk","properties":{}}]

@ -1,21 +1,24 @@
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'
apply plugin: 'io.fabric' // Crashlytics
apply plugin: 'com.google.gms.google-services' // Crashlytics apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
repositories { repositories {
maven { url 'https://maven.fabric.io/public' }
maven { url 'https://jitpack.io' } // required for MPAndroidChart maven { url 'https://jitpack.io' } // required for MPAndroidChart
jcenter() // for kotlin serialization jcenter() // for kotlin serialization
} }
android { android {
compileSdkVersion 29 compileSdkVersion 35
buildToolsVersion "28.0.3" buildToolsVersion "30.0.3"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,22 +29,22 @@ 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 29 targetSdkVersion 35
versionCode 102 versionCode 180
versionName "5.0.7" versionName "6.0.38"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
debug { debug {
ext.enableCrashlytics = false ext.enableCrashlytics = false
firebaseCrashlytics {
mappingFileUploadEnabled false // should help speed up build times: https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=en&platform=android
}
} }
release { release {
minifyEnabled true minifyEnabled true
@ -52,7 +55,7 @@ android {
def date = new Date() def date = new Date()
def formattedDate = date.format('yyMMdd_HHmm') def formattedDate = date.format('yyMMdd_HHmm')
def appName = "PokerAnalytics" def appName = "PokerAnalytics"
def buildType = variant.variantData.variantConfiguration.buildType.name def buildType = variant.buildType.name
def newName def newName
if (buildType == 'debug'){ if (buildType == 'debug'){
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk" newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk"
@ -65,20 +68,15 @@ android {
} }
} }
flavorDimensions 'endOfUse' flavorDimensions 'endOfUse'
productFlavors { // already used: 50000, 51000, 52000 productFlavors { // already used: 50000, 51000, 52000, 52130, 52110, 52120
standard { standard {
dimension = 'endOfUse' dimension = 'endOfUse'
} }
april2021 { // oct2021 {
dimension = 'endOfUse' // dimension = 'endOfUse'
versionNameSuffix = '_april2021' // versionNameSuffix = '_oct2021'
versionCode = 52100 + android.defaultConfig.versionCode // versionCode = 52120 + android.defaultConfig.versionCode
} // }
nov2020 {
dimension = 'endOfUse'
versionNameSuffix = '_nov2020'
versionCode = 52110 + android.defaultConfig.versionCode
}
} }
configurations { configurations {
@ -87,40 +85,50 @@ android {
} }
} }
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin // Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency // implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
// Android // Android
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.core:core-ktx:1.3.1'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.3.0'
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.4.0' implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews // implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places // 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
// WARNING FOR 2.0: https://developer.android.com/google/play/billing/billing_library_releases_notes implementation 'com.android.billingclient:billing:7.0.0'
// Purchases MUST BE ACKNOWLEDGED
implementation 'com.android.billingclient:billing:1.2.2'
// Firebase // Import the BoM for the Firebase platform
implementation 'com.google.firebase:firebase-core:17.5.0' implementation platform('com.google.firebase:firebase-bom:26.1.0')
// Declare the dependencies for the Crashlytics and Analytics libraries
// Crashlytics // When using the BoM, you don't specify versions in Firebase library dependencies
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// Logs // Logs
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
@ -135,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:mobile-ffmpeg-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.2.0' androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' 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'

@ -8,20 +8,12 @@
"client": [ "client": [
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:245968016816:android:47f8b4f74b1296b4", "mobilesdk_app_id": "1:245968016816:android:e5597a41d79df0a31d7275",
"android_client_info": { "android_client_info": {
"package_name": "net.pokeranalytics.android" "package_name": "net.pokeranalytics.android"
} }
}, },
"oauth_client": [ "oauth_client": [
{
"client_id": "245968016816-tr2mo4kbe1acn8u3ebbd9nk29iuk8fqr.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "net.pokeranalytics.android",
"certificate_hash": "e26278fa6db56acde23b0ff5981692f7f60408b9"
}
},
{ {
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com", "client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3 "client_type": 3
@ -33,20 +25,20 @@
} }
], ],
"services": { "services": {
"analytics_service": {
"status": 1
},
"appinvite_service": { "appinvite_service": {
"status": 2,
"other_platform_oauth_client": [ "other_platform_oauth_client": [
{ {
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com", "client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3 "client_type": 3
},
{
"client_id": "245968016816-8sklai9sq70m46anv550uttdic6cukn6.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "stax.SlashPoker.nosebleed"
}
} }
] ]
},
"ads_service": {
"status": 2
} }
} }
} }

@ -30,6 +30,7 @@
-dontwarn io.realm.** -dontwarn io.realm.**
-keep class net.pokeranalytics.android.model.** { *; } -keep class net.pokeranalytics.android.model.** { *; }
-keep class net.pokeranalytics.android.ui.fragment.** { *; } -keep class net.pokeranalytics.android.ui.fragment.** { *; }
-keep class net.pokeranalytics.android.ui.modules.** { *; }
-keepattributes SourceFile,LineNumberTable # Keep file names and line numbers. -keepattributes SourceFile,LineNumberTable # Keep file names and line numbers.
-keep public class * extends java.lang.Exception # Optional: Keep custom exceptions. -keep public class * extends java.lang.Exception # Optional: Keep custom exceptions.
@ -63,4 +64,26 @@
-keep class com.google.j2objc.annotations.** { *; } -keep class com.google.j2objc.annotations.** { *; }
# 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(...);
}

@ -30,12 +30,9 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
s2.endDate = Date() s2.endDate = Date()
s3.endDate = Date() s3.endDate = Date()
s1.cgBigBlind = 4.0 s1.cgBlinds = "2/4"
s1.cgSmallBlind = 2.0 s2.cgBlinds = "2/4"
s2.cgBigBlind = 4.0 s3.cgBlinds = "1/2"
s2.cgSmallBlind = 2.0
s3.cgBigBlind = 1.0
s3.cgSmallBlind = 1.0
realm.insert(s1) realm.insert(s1)
realm.insert(s2) realm.insert(s2)
@ -45,7 +42,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext) val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) { if (favSession != null) {
Assert.assertEquals(4.0, favSession.cgBigBlind) Assert.assertEquals(4.0, favSession.cgBiggestBet)
} else { } else {
Assert.fail("session shouldn't be null") Assert.fail("session shouldn't be null")
} }
@ -65,9 +62,9 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val loc1 = realm.createObject(Location::class.java, "1") val loc1 = realm.createObject(Location::class.java, "1")
val loc2 = realm.createObject(Location::class.java, "2") val loc2 = realm.createObject(Location::class.java, "2")
s1.cgBigBlind = 4.0 s1.cgBiggestBet = 4.0
s2.cgBigBlind = 4.0 s2.cgBiggestBet = 4.0
s3.cgBigBlind = 1.0 s3.cgBiggestBet = 1.0
s1.location = loc1 s1.location = loc1
s2.location = loc1 s2.location = loc1
@ -81,7 +78,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext) val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) { if (favSession != null) {
Assert.assertEquals(1.0, favSession.cgBigBlind) Assert.assertEquals(1.0, favSession.cgBiggestBet)
} else { } else {
Assert.fail("session shouldn't be null") Assert.fail("session shouldn't be null")
} }

@ -44,8 +44,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s2.result?.buyin = 200.0 s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300 s2.result?.cashout = 500.0 // net result = 300
s1.cgBigBlind = 0.5 // bb net result = -200bb s1.cgBlinds = "0.5" // bb net result = -200bb
s2.cgBigBlind = 2.0 // bb net result = 150bb s2.cgBlinds = "2.0" // bb net result = 150bb
s2.tableSize = 5 s2.tableSize = 5
@ -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")

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import java.util.* import java.util.*
@ -28,28 +28,25 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0 s1.cgBlinds = "0.5/1"
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 1.0 s2.cgBlinds = "0.5/1"
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1) val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBigBlind = 2.0 s3.cgBlinds = "1/2"
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyBlind() val filter = QueryCondition.AnyStake()
val blind = QueryCondition.AnyBlind().apply { val blind = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.blinds!!) listOfValues = arrayListOf(s1.blinds!!)
} }
blind.filterSectionRow = FilterSectionRow.Blind // blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind)) val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -74,33 +71,27 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0 s1.cgBlinds = "0.5/1"
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 1.0 s2.cgBlinds = "0.5/1"
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1) val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBigBlind = 2.0 s3.cgBlinds = "1/2"
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyBlind() val filter = QueryCondition.AnyStake()
val blind1 = QueryCondition.AnyBlind().apply { val blind1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.blinds!!) listOfValues = arrayListOf(s1.blinds!!)
} }
val blind2 = QueryCondition.AnyBlind().apply { val blind2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.blinds!!) listOfValues = arrayListOf(s2.blinds!!)
} }
blind1.filterSectionRow = FilterSectionRow.Blind val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Stakes)
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElements = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueBy(filterElements) filter.updateValueBy(filterElements)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -125,30 +116,24 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1"
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 1.0 s2.cgBlinds = "0.5/1"
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b2) val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBigBlind = 2.0 s3.cgBlinds = "1/2"
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyBlind() val filter = QueryCondition.AnyStake()
val blind = QueryCondition.AnyBlind().apply { val blind = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s3.blinds!!) listOfValues = arrayListOf(s3.blinds!!)
} }
blind.filterSectionRow = FilterSectionRow.Blind val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -172,34 +157,27 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0 s1.cgBlinds = "0.5/1"
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 2.0 s2.cgBlinds = "0.5/1"
s2.cgSmallBlind = 1.0
val s3 = Session.testInstance(100.0, false, Date(), 1, b2) val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBigBlind = 2.0 s3.cgBlinds = "1/2"
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val filter = QueryCondition.AnyBlind() val stake1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.cgStakes!!)
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
} }
val blind2 = QueryCondition.AnyBlind().apply { val stake2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.blinds!!) listOfValues = arrayListOf(s2.cgStakes!!)
} }
blind1.filterSectionRow = FilterSectionRow.Blind val filterElement = FilterCondition(arrayListOf(stake1, stake2), FilterSectionRow.Stakes)
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import net.pokeranalytics.android.util.extensions.hourMinute import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.extensions.startOfDay import net.pokeranalytics.android.util.extensions.startOfDay
import org.junit.Assert import org.junit.Assert
@ -35,8 +35,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) } val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) }
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -64,8 +63,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) } val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) }
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -92,8 +90,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyYear() val filter = QueryCondition.AnyYear()
cal.time = s1.startDate cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) } val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) }
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -384,10 +381,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s2 = Session.testInstance(100.0, true, cal.time, 1) val s2 = Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.StartedFromDate() val filter = QueryCondition.StartedFromDate(Date())
val filterElementRow = QueryCondition.StartedFromDate().apply { singleValue = s2.startDate!!} val filterElementRow = QueryCondition.StartedFromDate(Date()).apply { singleValue = s2.startDate!!}
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -412,10 +408,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.StartedToDate() val filter = QueryCondition.StartedToDate(Date())
val filterElementRow = QueryCondition.StartedToDate().apply { singleValue = s1.startDate!! } val filterElementRow = QueryCondition.StartedToDate(Date()).apply { singleValue = s1.startDate!! }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -441,10 +436,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.EndedFromDate() val filter = QueryCondition.EndedFromDate(Date())
val filterElementRow = QueryCondition.EndedFromDate().apply { singleValue = s2.endDate() } val filterElementRow = QueryCondition.EndedFromDate(Date()).apply { singleValue = s2.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -470,10 +464,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.EndedToDate() val filter = QueryCondition.EndedToDate(Date())
val filterElementRow = QueryCondition.EndedToDate().apply { singleValue = s1.endDate() } val filterElementRow = QueryCondition.EndedToDate(Date()).apply { singleValue = s1.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -5,8 +5,9 @@ import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow import net.pokeranalytics.android.ui.view.rows.FixedValueFilterItemRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -24,8 +25,9 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
filter.name = "testSaveLoadCashFilter" filter.name = "testSaveLoadCashFilter"
val filterElement = QueryCondition.IsCash val filterElement = QueryCondition.IsCash
filterElement.filterSectionRow = FilterSectionRow.CashOrTournament val filterItemRow = FixedValueFilterItemRow(filterElement, FilterSectionRow.CashOrTournament)
filter.createOrUpdateFilterConditions(arrayListOf(filterElement))
filter.createOrUpdateFilterConditions(arrayListOf(filterItemRow))
val useCount = filter.countBy(FilterCategoryRow.GENERAL) val useCount = filter.countBy(FilterCategoryRow.GENERAL)
Assert.assertEquals(1, useCount) Assert.assertEquals(1, useCount)

@ -6,7 +6,7 @@ import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -110,8 +110,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll() val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) } val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filterElementRow.filterSectionRow = FilterSectionRow.Bankroll filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Bankroll))
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -141,11 +140,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll() val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) } val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) } val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) }
filterElementRow2.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Bankroll))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -169,7 +166,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val anyGame = QueryCondition.AnyGame(g2) val anyGame = QueryCondition.AnyGame(g2)
val fc = FilterCondition(filterElementRows = arrayListOf(anyGame)) val fc = FilterCondition(arrayListOf(anyGame), FilterSectionRow.Game)
val sessions = Filter.queryOn<Session>(realm, Query(fc.queryCondition)) val sessions = Filter.queryOn<Session>(realm, Query(fc.queryCondition))
Assert.assertEquals(1, sessions.size) Assert.assertEquals(1, sessions.size)
@ -197,10 +194,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) } val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) }
filterElementRow.filterSectionRow = FilterSectionRow.Game
val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) } val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) }
filterElementRow2.filterSectionRow = FilterSectionRow.Game
val filterCondition = FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)) val filterCondition = FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Game)
val queryCondition = filterCondition.queryCondition val queryCondition = filterCondition.queryCondition
val sessions = Filter.queryOn<Session>(realm, Query(queryCondition)) val sessions = Filter.queryOn<Session>(realm, Query(queryCondition))
@ -225,8 +222,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation() val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) } val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.Location filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Location))
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -257,11 +253,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation() val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) } val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.Location
val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) } val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) }
filterElementRow2.filterSectionRow = FilterSectionRow.Location
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Location))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -287,8 +281,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName() val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) } val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentName))
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -318,10 +311,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName() val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) } val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) } val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TournamentName))
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -354,12 +345,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AllTournamentFeature() val filter = QueryCondition.AllTournamentFeature()
val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) } val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) } val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) } val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) }
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3))) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3), FilterSectionRow.TournamentFeature))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -389,14 +378,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature() val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) } val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) } val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) } val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) }
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) } val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) }
filterElementRow4.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4))) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4), FilterSectionRow.TournamentFeature))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -423,8 +409,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature() val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) } val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentFeature))
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -448,10 +433,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTableSize() val filter = QueryCondition.AnyTableSize()
val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) } val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) }
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) } val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) }
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))) filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TableSize))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -474,9 +458,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.NetAmountWon() val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) } val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow)) val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow))
@ -499,9 +482,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.NetAmountWon() val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) } val filterElementRow = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -524,14 +506,12 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filterMore = QueryCondition.NetAmountWon() val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) } val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filterLess = QueryCondition.NetAmountWon() val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) } val filterElementRow2 = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterElementRow2.filterSectionRow = FilterSectionRow.Value filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value))
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess)) val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess))
@ -556,11 +536,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s3 = Session.testInstance(netResult = 500.0) val s3 = Session.testInstance(netResult = 500.0)
s3.cgBigBlind = 2.0 s3.cgBlinds = "2.0"
s3.result!!.buyin = 1000.0 s3.result!!.buyin = 1000.0
val s4 = Session.testInstance(netResult = 570.0) val s4 = Session.testInstance(netResult = 570.0)
s4.cgBigBlind = 5.0 s4.cgBlinds = "5.0"
s4.result!!.buyin = 200.0 s4.result!!.buyin = 200.0
realm.commitTransaction() realm.commitTransaction()
@ -604,7 +584,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10) val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10)
sessions = Filter.queryOn<Session>(realm, Query(filterMore)) sessions = Filter.queryOn(realm, Query(filterMore))
Assert.assertEquals(1, sessions.size) Assert.assertEquals(1, sessions.size)

@ -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,13 +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="com.android.vending.BILLING" />
<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" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<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"
@ -21,13 +26,14 @@
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="false" /> android:value="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity" android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait"> android:screenOrientation="portrait"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -40,7 +46,8 @@
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity" android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait"> android:screenOrientation="portrait"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError"> <intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -55,121 +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:windowSoftInputMode="adjustNothing" /> 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="singleTop" 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
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop"
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

@ -3,21 +3,19 @@ package net.pokeranalytics.android
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import com.crashlytics.android.Crashlytics import com.google.firebase.FirebaseApp
import com.crashlytics.android.core.CrashlyticsCore
import io.fabric.sdk.android.Fabric
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.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.FakeDataManager import net.pokeranalytics.android.util.*
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.*
@ -25,6 +23,9 @@ import java.util.*
class PokerAnalyticsApplication : Application() { class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object { companion object {
fun timeSinceInstall(context: Context): Long { fun timeSinceInstall(context: Context): Long {
@ -36,6 +37,11 @@ class PokerAnalyticsApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
UserDefaults.init(this) UserDefaults.init(this)
// AppGuard / Billing services // AppGuard / Billing services
@ -45,35 +51,23 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this) Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME) .name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(9) .schemaVersion(14)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
.build() .build()
Realm.setDefaultConfiguration(realmConfiguration) Realm.setDefaultConfiguration(realmConfiguration)
// val realm = Realm.getDefaultInstance()
// realm.executeTransaction {
// realm.where(Session::class.java).findAll().deleteAllFromRealm()
// }
// realm.close()
// Set up Crashlytics, disabled for debug builds
val crashlyticsKit = Crashlytics.Builder()
.core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
.build()
// Initialize Fabric with the debug-disabled crashlytics.
Fabric.with(this, crashlyticsKit)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val locales = resources.configuration.locales val locales = resources.configuration.locales
Crashlytics.log("App onCreate. Locales = $locales") CrashLogging.log("App onCreate. Locales = $locales")
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// Logs // Logs
Timber.plant(PokerAnalyticsLogs()) Timber.plant(PokerAnalyticsLogs())
} }
Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
@ -82,10 +76,23 @@ class PokerAnalyticsApplication : Application() {
// this.createFakeSessions() // this.createFakeSessions()
} }
Patcher.patchAll(this.applicationContext) // Patch
Patcher.patchAll(this)
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// Infos
val locale = Locale.getDefault() val locale = Locale.getDefault()
Crashlytics.log("Country: ${locale.country}, language: ${locale.language}") CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
} }
@ -99,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
}
}
}

@ -0,0 +1,49 @@
package net.pokeranalytics.android.api
import android.content.Context
import com.android.volley.Request
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.Volley
import org.json.JSONArray
import timber.log.Timber
data class BlogPost(var id: Int, var content: String)
private fun JSONArray.toBlogPosts(): List<BlogPost> {
val posts = mutableListOf<BlogPost>()
(0 until this.length()).forEach { index ->
val jo = this.getJSONObject(index)
val post = BlogPost(jo.getInt("id"), jo.getJSONObject("content").getString("rendered"))
posts.add(post)
}
return posts
}
class BlogPostApi {
companion object {
private const val tipsLastPostsURL = "https://www.poker-analytics.net/blog/wp-json/wp/v2/posts/?categories=109\n"
fun getLatestPosts(context: Context, callback: (List<BlogPost>) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val jsonObjectRequest = JsonArrayRequest(
Request.Method.GET, tipsLastPostsURL, null,
{ response ->
// Timber.d("posts = $response")
callback(response.toBlogPosts())
},
{ error ->
Timber.w("Error while retrieving blog posts: $error")
}
)
queue.add(jsonObjectRequest)
}
}
}

@ -0,0 +1,60 @@
package net.pokeranalytics.android.api
import android.content.Context
import androidx.annotation.Keep
import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
@Keep
@Serializable
data class RateResponse(var info: RateInfo)
@Keep
@Serializable
data class RateInfo(var rate: Double)
class CurrencyConverterApi {
companion object {
val json = Json { ignoreUnknownKeys = true }
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
Timber.d("Api call = $url")
val stringRequest = object : StringRequest(
Method.GET, url,
{ response ->
val o = json.decodeFromString<RateResponse>(response)
Timber.d("rate = ${o.info.rate}")
callback(o.info.rate, null)
},
{
Timber.d("Api call failed: ${it.message}")
callback(null, it)
}) {
override fun getHeaders(): MutableMap<String, String> {
val headers = HashMap<String, String>()
headers["apikey"] = "XnfeyID3PMKd3k4zTPW0XmZAbcZlZgqH"
return headers
}
}
queue.add(stringRequest)
}
}
}

@ -1,47 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import timber.log.Timber
class FreeConverterApi {
companion object {
fun currencyRate(pair: String, context: Context, callback: (Double) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=5ba8d38995282fe8b1c8"
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 }
val stringRequest = StringRequest(
Request.Method.GET, url,
Response.Listener { response ->
val json = Json(JsonConfiguration.Stable)
val f = json.parseJson(response)
f.jsonObject[pair]?.primitive?.double?.let { rate ->
callback(rate)
} ?: run {
Timber.d("no rate: $response")
}
},
Response.ErrorListener {
Timber.d("Api call failed: ${it.message}")
})
queue.add(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
}
}
}

@ -2,7 +2,7 @@ package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.ui.graph.AxisFormatting import net.pokeranalytics.android.ui.graph.Graph
enum class AggregationType { enum class AggregationType {
SESSION, SESSION,
@ -20,14 +20,6 @@ enum class AggregationType {
} }
} }
val axisFormatting: AxisFormatting
get() {
return when (this) {
DURATION -> AxisFormatting.X_DURATION
else -> AxisFormatting.DEFAULT
}
}
val criterias: List<Criteria> val criterias: List<Criteria>
get() { get() {
return when (this) { return when (this) {

@ -2,7 +2,6 @@ package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
@ -10,15 +9,8 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.filter import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.extensions.startOfDay import net.pokeranalytics.android.util.extensions.startOfDay
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -34,18 +26,18 @@ class Calculator {
* The options used for calculations and display * The options used for calculations and display
*/ */
class Options( class Options(
var display: Display = Display.TABLE, var progressValues: ProgressValues = ProgressValues.NONE,
progressValues: ProgressValues = ProgressValues.NONE,
var stats: List<Stat> = listOf(), var stats: List<Stat> = listOf(),
var criterias: List<Criteria> = listOf(), var criterias: List<Criteria> = listOf(),
var query: Query = Query(), var query: Query = Query(),
var filterId: String? = null, var filterId: String? = null,
private var aggregationType: AggregationType? = null, private var aggregationType: AggregationType? = null,
var userGenerated: Boolean = false, var userGenerated: Boolean = false,
var reportSetupId: String? = null var reportSetupId: String? = null,
var includedTransactions: List<TransactionType> = listOf()
) { ) {
constructor(display: Display = Display.TABLE, constructor(
progressValues: ProgressValues = ProgressValues.NONE, progressValues: ProgressValues = ProgressValues.NONE,
stats: List<Stat> = listOf(), stats: List<Stat> = listOf(),
criterias: List<Criteria> = listOf(), criterias: List<Criteria> = listOf(),
@ -53,18 +45,18 @@ class Calculator {
aggregationType: AggregationType? = null, aggregationType: AggregationType? = null,
userGenerated: Boolean = false, userGenerated: Boolean = false,
reportSetupId: String? = null) : reportSetupId: String? = null) :
this(display, progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId) this(progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId)
/** /**
* Specifies whether progress values should be added and their kind * Specifies whether progress values should be added and their kind
*/ */
var progressValues: ProgressValues = progressValues // var progressValues: ProgressValues = progressValues
get() { // get() {
if (field == ProgressValues.NONE && this.display == Display.PROGRESS) { // if (field == ProgressValues.NONE && this.display.requireProgressValues) {
return ProgressValues.STANDARD // return ProgressValues.STANDARD
} // }
return field // return field
} // }
init { init {
this.aggregationType?.let { this.aggregationType?.let {
@ -72,40 +64,6 @@ class Calculator {
} }
} }
/**
* The way the computed stats are going to be displayed
*/
enum class Display : RowRepresentable {
TABLE,
PROGRESS,
COMPARISON,
MAP,
POLYNOMIAL;
override val resId: Int?
get() {
return when (this) {
TABLE -> R.string.table
PROGRESS -> R.string.progress
COMPARISON -> R.string.comparison
MAP -> R.string.map
POLYNOMIAL -> null
}
}
val activityClass: Class<*>
get() {
return when (this) {
TABLE -> TableReportActivity::class.java
PROGRESS -> ProgressReportActivity::class.java
COMPARISON -> ComparisonReportActivity::class.java
else -> throw PAIllegalStateException("undefined activity for report display")
// MAP -> R.string.map
// POLYNOMIAL -> null
}
}
}
/** /**
* The type of evolution numericValues * The type of evolution numericValues
@ -136,6 +94,7 @@ class Calculator {
get() { get() {
return this.stats.contains(LONGEST_STREAKS) return this.stats.contains(LONGEST_STREAKS)
} }
/** /**
* Whether the values should be sorted * Whether the values should be sorted
*/ */
@ -143,6 +102,7 @@ class Calculator {
get() { get() {
return this.progressValues != ProgressValues.NONE || this.computeLongestStreak return this.progressValues != ProgressValues.NONE || this.computeLongestStreak
} }
/** /**
* Whether the number of locations played should be computed * Whether the number of locations played should be computed
*/ */
@ -192,7 +152,6 @@ class Calculator {
): Report { ): Report {
val options = Options( val options = Options(
display = Options.Display.PROGRESS,
progressValues = Options.ProgressValues.STANDARD, progressValues = Options.ProgressValues.STANDARD,
stats = listOf(stat), stats = listOf(stat),
aggregationType = aggregationType aggregationType = aggregationType
@ -218,13 +177,13 @@ class Calculator {
val computableGroups: MutableList<ComputableGroup> = mutableListOf() val computableGroups: MutableList<ComputableGroup> = mutableListOf()
options.criterias.combined().forEach { comparatorQuery -> val combinations = options.criterias.combined()
// Timber.d("Combinations: ${ combinations.map { it.defaultName }}")
for (comparatorQuery in combinations) {
comparatorQuery.merge(options.query) comparatorQuery.merge(options.query)
val group = ComputableGroup(comparatorQuery) val group = ComputableGroup(comparatorQuery)
computableGroups.add(group) computableGroups.add(group)
} }
if (computableGroups.size == 0) { if (computableGroups.size == 0) {
@ -242,7 +201,6 @@ class Calculator {
val report = Report(options) val report = Report(options)
groups.forEach { group -> groups.forEach { group ->
// val s = Date()
// Clean existing computables / sessionSets if group is reused // Clean existing computables / sessionSets if group is reused
group.cleanup() group.cleanup()
@ -282,14 +240,27 @@ class Calculator {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues) val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val computables = computableGroup.computables(realm, options.shouldSortValues) val computables = computableGroup.computables(realm, options.shouldSortValues)
Timber.d("#### Start computing group, ${computables.size} computables")
if (computables.size == 0) { // we don't want to return stats with 0 as a value when comparing best performances
return results
}
// Timber.d("#### Start computing group, ${computables.size} computables")
results.addStat(NUMBER_OF_GAMES, computables.size.toDouble()) results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())
// computables.forEach { // computables.forEach {
// Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}") // Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}")
// } // }
val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble() var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
results.addStat(NET_RESULT, sum) if (options.includedTransactions.isNotEmpty()) {
for (transactionType in options.includedTransactions) {
val transactions = computableGroup.transactions(realm, transactionType, options.shouldSortValues)
val transactionRatedAmount = transactions.sum(Transaction.Field.RATED_AMOUNT.identifier).toDouble()
ratedNet += transactionRatedAmount
}
}
results.addStat(NET_RESULT, ratedNet)
val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble() val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
results.addStat(HANDS_PLAYED, totalHands) results.addStat(HANDS_PLAYED, totalHands)
@ -305,39 +276,52 @@ class Calculator {
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble() val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(TOTAL_BUYIN, totalBuyin) results.addStat(TOTAL_BUYIN, totalBuyin)
Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}")
val totalTips = computables.sum(ComputableResult.Field.RATED_TIPS.identifier).toDouble()
results.addStat(TOTAL_TIPS, totalTips)
// Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}")
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
maxNetResult?.let { maxNetResult?.let {
results.addStat(MAXIMUM_NETRESULT, it) results.addStat(MAXIMUM_NET_RESULT, it)
} }
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let { minNetResult?.let {
results.addStat(MINIMUM_NETRESULT, it) results.addStat(MINIMUM_NET_RESULT, it)
} }
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 -> Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStat(NET_BB_PER_100_HANDS, netBB100) results.addStat(NET_BB_PER_100_HANDS, netBB100)
} }
Stat.returnOnInvestment(sum, totalBuyin)?.let { roi -> Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi ->
results.addStat(ROI, roi) results.addStat(ROI, roi)
} }
// val shouldComputeITMRatio = options.stats.contains(TOURNAMENT_ITM_RATIO) || computableGroup.displayedStats?.contains(TOURNAMENT_ITM_RATIO) == true
// if (shouldComputeITMRatio) {
// val itmCount = computables.count { it.session?.result?.cashout ?: 0.0 > 0.0 } // should we add a property inside ComputableResult for better performance?
// val itmRatio = itmCount.toDouble() / computables.size.toDouble()
// results.addStat(TOURNAMENT_ITM_RATIO, itmRatio)
// }
if (options.computeLocationsPlayed) { if (options.computeLocationsPlayed) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble()) results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
} }
var average = 0.0 // also used for standard deviation later var average = 0.0 // also used for standard deviation later
if (computables.size > 0) { if (computables.size > 0) {
average = sum / computables.size.toDouble() average = ratedNet / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble() val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble() val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats( results.addStats(
setOf( setOf(
ComputedStat(AVERAGE, average), ComputedStat(AVERAGE, average),
ComputedStat(WIN_RATIO, winRatio), ComputedStat(WIN_RATIO, winRatio),
ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio),
ComputedStat(AVERAGE_BUYIN, avgBuyin) ComputedStat(AVERAGE_BUYIN, avgBuyin)
) )
) )
@ -364,6 +348,7 @@ class Calculator {
var longestWinStreak = 0 var longestWinStreak = 0
var longestLoseStreak = 0 var longestLoseStreak = 0
var currentStreak = 0 var currentStreak = 0
var tITMCount = 0
computables.forEach { computable -> computables.forEach { computable ->
index++ index++
@ -371,6 +356,7 @@ class Calculator {
tBBSum += computable.bbNet tBBSum += computable.bbNet
tBBSessionCount += computable.hasBigBlind tBBSessionCount += computable.hasBigBlind
tWinningSessionCount += computable.isPositive tWinningSessionCount += computable.isPositive
tITMCount += computable.isPositive
tBuyinSum += computable.ratedBuyin tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands tHands += computable.estimatedHands
@ -396,11 +382,16 @@ 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,
data = session data = session
) )
results.addEvolutionValue(
tITMCount.toDouble() / index.toDouble(),
stat = TOURNAMENT_ITM_RATIO,
data = session)
results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session) results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session)
results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session) results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session)
@ -442,9 +433,9 @@ class Calculator {
} }
} }
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() || val shouldIterateOverSets = computableGroup.conditions.isNotEmpty()
options.progressValues != Options.ProgressValues.NONE || || options.progressValues != Options.ProgressValues.NONE
options.computeDaysPlayed || options.computeDaysPlayed
// Session Set // Session Set
if (shouldIterateOverSets) { if (shouldIterateOverSets) {
@ -491,10 +482,6 @@ class Calculator {
) )
results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet) results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet)
Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = sessionSet)
}
} }
Options.ProgressValues.TIMED -> { Options.ProgressValues.TIMED -> {
results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet) results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet)
@ -546,7 +533,7 @@ class Calculator {
var hourlyRate = 0.0 var hourlyRate = 0.0
if (gHourlyDuration != null) { if (gHourlyDuration != null) {
hourlyRate = sum / gHourlyDuration hourlyRate = ratedNet / gHourlyDuration
if (sessionSets.size > 0) { if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate) results.addStat(HOURLY_RATE, hourlyRate)
@ -632,10 +619,10 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) { if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet) this.initStatsWithSet(sessionSet)
} else { } else {
ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 } ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumByDouble { it.bbNet } bbSum = filteredSessions.sumOf { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumByDouble { it.estimatedHands } estimatedHands = filteredSessions.sumOf { it.estimatedHands }
} }
} }
} }

@ -0,0 +1,111 @@
package net.pokeranalytics.android.calculus
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import timber.log.Timber
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The size of the retrieved computables list
*/
var size: Int = 0
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this.size = computables.size
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
this._sessionSets = sets
return sets
}
/**
* Retrieves the transactions on the relative [realm] filtered with the provided [conditions]
*/
fun transactions(realm: Realm, transactionType: TransactionType, sorted: Boolean = false): RealmResults<Transaction> {
val query = this.query.copy()
query.add(QueryCondition.AnyTransactionType(transactionType))
val sortedField = if (sorted) "date" else null
Timber.d("query = ${query.defaultName}")
return Filter.queryOn(realm, query, sortedField)
}
/**
* Nullifies used Realm results
*/
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
}

@ -1,25 +1,13 @@
package net.pokeranalytics.android.calculus package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import com.github.mikephil.charting.data.*
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import kotlin.math.abs
/** /**
* The class returned after performing calculation in the Calculator object * The class returned after performing calculation in the Calculator object
@ -43,138 +31,28 @@ class Report(var options: Calculator.Options) {
this._results.add(result) this._results.add(result)
} }
/** fun max(stat: Stat): ComputedResults? {
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result var computedResults: ComputedResults? = null
*/ var count = 0
fun lineEntries(stat: Stat? = null, context: Context): LineDataSet { var max = Double.MIN_VALUE
val entries = mutableListOf<Entry>() for (cr in this._results) {
val statToUse = stat ?: options.stats.firstOrNull() cr.computedStat(stat)?.value?.let { value ->
val statName = statToUse?.name ?: "" count += 1
if (value > max) {
statToUse?.let { computedResults = cr
this._results.forEachIndexed { index, results -> max = value
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.firstOrNull()
statToUse?.let {
this._results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
} }
} }
} }
return if (count >= 2) { computedResults } else { null }
val label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this._results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
} }
} }
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(var query: Query, var stats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
this._sessionSets = sets
return sets
}
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
} }
class ComputedResults(group: ComputableGroup, class ComputedResults(group: ComputableGroup,
@ -196,6 +74,10 @@ class ComputedResults(group: ComputableGroup,
private val allStats: Collection<ComputedStat> private val allStats: Collection<ComputedStat>
get() { return this._computedStats.values } get() { return this._computedStats.values }
val evolutionValues: Map<Stat, MutableList<Point>>
get() {
return this._evolutionValues
}
/** /**
* Adds a value to the evolution values * Adds a value to the evolution values
@ -355,79 +237,6 @@ class ComputedResults(group: ComputableGroup,
this.consolidateProgressStats() this.consolidateProgressStats()
} }
// MPAndroidChart
fun defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}
val isEmpty: Boolean val isEmpty: Boolean
get() { get() {
return this.group.isEmpty return this.group.isEmpty
@ -449,18 +258,18 @@ class ComputedResults(group: ComputableGroup,
override fun legendValues( override fun legendValues(
stat: Stat, stat: Stat,
entry: Entry, total: Double,
style: GraphFragment.Style, style: Graph.Style,
groupName: String, groupName: String,
context: Context context: Context
): LegendContent { ): LegendContent {
when (style) { when (style) {
GraphFragment.Style.BAR -> { Graph.Style.BAR -> {
return when (stat) { return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue) DefaultLegendValues(this.entryTitle(context), totalStatValue)
} }
else -> { else -> {
@ -474,12 +283,12 @@ class ComputedResults(group: ComputableGroup,
return when (stat) { return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue) DefaultLegendValues(this.entryTitle(context), totalStatValue)
} }
else -> { else -> {
val entryValue = this.formattedValue(stat) val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue) DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue)
} }
} }
@ -488,10 +297,4 @@ class ComputedResults(group: ComputableGroup,
} }
} }
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
} }

@ -0,0 +1,338 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import android.os.CountDownTimer
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
interface NewPerformanceListener {
fun newBestPerformanceHandler()
}
class ReportWhistleBlower(var context: Context) {
private var sessions: RealmResults<Session>? = null
private var results: RealmResults<Result>? = null
private var currentTask: ReportTask? = null
private val currentNotifications: MutableList<String> = mutableListOf() // Performance.id
private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
var paused: Boolean = false
private var timer: CountDownTimer? = null
init {
val realm = Realm.getDefaultInstance()
this.sessions = realm.where(Session::class.java).findAll()
this.sessions?.addChangeListener { _ ->
requestReportLaunch()
}
this.results = realm.where(Result::class.java).findAll()
this.results?.addChangeListener { _ ->
requestReportLaunch()
}
realm.close()
}
fun addListener(newPerformanceListener: NewPerformanceListener) {
this.listeners.add(newPerformanceListener)
}
fun removeListener(listener: NewPerformanceListener) {
this.listeners.remove(listener)
}
fun requestReportLaunch() {
// Timber.d(">>> Launch report")
if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return
}
this.timer?.cancel()
val launchStart = 100L
val timer = object: CountDownTimer(launchStart, launchStart) {
override fun onTick(p0: Long) { }
override fun onFinish() {
launchReportTask()
}
}
this.timer = timer
timer.start()
}
private fun launchReportTask() {
synchronized(this) {
this.currentTask?.cancel()
val reportTask = ReportTask(this, this.context)
this.currentTask = reportTask
reportTask.start()
}
}
/**
* Pauses the whistleblower, for example when importing data
*/
fun pause() {
this.paused = true
this.currentTask?.cancel()
this.currentTask = null
}
fun resume() {
this.paused = false
this.requestReportLaunch()
}
fun has(performanceId: String): Boolean {
return this.currentNotifications.contains(performanceId)
}
fun notify(performance: Performance) {
this.currentNotifications.add(performance.id)
for (listener in this.listeners) {
listener.newBestPerformanceHandler()
}
}
fun clearNotifications() {
this.currentNotifications.clear()
}
}
class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Context) {
private var cancelled = false
var handler: (() -> Unit)? = null
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() {
messages.add("Starting task...")
launchReports()
}
fun cancel() {
this.cancelled = true
}
var messages: MutableList<String> = mutableListOf()
private fun launchReports() {
CoroutineScope(coroutineContext).launch {
val realm = Realm.getDefaultInstance()
// Basic
for (basicReport in StaticReport.basicReports) {
if (cancelled) {
break
}
launchReport(realm, basicReport)
}
// CustomField
val customFields = realm.where(CustomField::class.java)
.equalTo("type", CustomField.Type.LIST.uniqueIdentifier).findAll()
for (customField in customFields) {
if (cancelled) {
break
}
launchReport(realm, StaticReport.CustomFieldList(customField))
}
realm.close()
}
}
private fun launchReport(realm: Realm, report: StaticReport) {
// Timber.d(">>> launch report = $report")
when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, report)
else -> launchDefaultReport(realm, report)
}
}
private fun launchDefaultReport(realm: Realm, report: StaticReport) {
val options = Calculator.Options(
stats = report.stats,
criterias = report.criteria
)
val result = Calculator.computeStats(realm, options = options)
analyseDefaultReport(realm, report, result)
}
private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.entries.forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive)
analyseOptimalDuration(realm, report, key, duration)
}
this.handler?.let { it() }
}
private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) {
messages.add("Analyse report $staticReport...")
val nameSeparator = " "
for (stat in result.options.stats) {
// Timber.d("analyse stat: $stat for report: $staticReport")
// Get current performance
var query = performancesQuery(realm, staticReport, stat)
val customField: CustomField? =
(staticReport as? StaticReport.CustomFieldList)?.customField
customField?.let {
query = query.equalTo("customFieldId", it.id)
}
val currentPerf = query.findFirst()
// Store if necessary, delete if necessary
val bestComputedResults = result.max(stat)
bestComputedResults?.let { computedResults ->
messages.add("found new perf...")
val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator)
Timber.d("Best computed = $performanceName, ${computedResults.computedStat(Stat.NET_RESULT)?.value}")
var storePerf = true
currentPerf?.let {
messages.add("has current perf...")
currentPerf.name?.let { name ->
if (computedResults.group.query.getName(this.context, nameSeparator) == name) {
storePerf = false
}
}
currentPerf.objectId?.let { objectId ->
if (computedResults.group.query.objectId == objectId) {
storePerf = false
}
}
if (storePerf) {
realm.executeTransaction {
currentPerf.name = performanceName
currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id
}
this.whistleBlower.notify(currentPerf)
}
}
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) {
val performance = Performance(
staticReport,
stat,
performanceName,
performanceQuery.objectId,
customField?.id,
null
)
realm.executeTransaction { it.copyToRealm(performance) }
this.whistleBlower.notify(performance)
}
} ?: run { // if there is no max but a now irrelevant Performance, we delete it
messages.add("deletes current perf if necessary: $currentPerf...")
// Timber.d("NO best computed value, current perf = $currentPerf ")
currentPerf?.let { perf ->
realm.executeTransaction {
// Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
perf.deleteFromRealm()
}
}
}
}
}
private fun analyseOptimalDuration(realm: Realm, staticReport: StaticReport, key: PerformanceKey, duration: Double?) {
val performance = performancesQuery(realm, staticReport, key).findFirst()
duration?.let {
var storePerf = true
val formattedDuration = (duration / 3600 / 1000).formattedHourlyDuration()
performance?.let { perf ->
if (perf.value == duration) {
storePerf = false
}
if (storePerf) {
realm.executeTransaction {
perf.name = formattedDuration
perf.value = duration
}
}
}
if (storePerf) {
val perf = Performance(staticReport, key, name = formattedDuration, value = duration)
realm.executeTransaction { it.copyToRealm(perf) }
this.whistleBlower.notify(perf)
}
} ?: run { // no duration
performance?.let { perf ->
realm.executeTransaction {
perf.deleteFromRealm() // delete if the perf exists
}
}
}
}
private fun performancesQuery(realm: Realm, staticReport: StaticReport, key: PerformanceKey): RealmQuery<Performance> {
return realm.where(Performance::class.java)
.equalTo("reportId", staticReport.uniqueIdentifier)
.equalTo("key", key.value)
}
}

@ -4,6 +4,7 @@ import android.content.Context
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.FormattingException import net.pokeranalytics.android.exceptions.FormattingException
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.PerformanceKey
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.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
@ -22,7 +23,7 @@ class StatFormattingException(message: String) : Exception(message)
/** /**
* An enum representing all the types of Session statistics * An enum representing all the types of Session statistics
*/ */
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable { enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable, PerformanceKey {
NET_RESULT(1), NET_RESULT(1),
BB_NET_RESULT(2), BB_NET_RESULT(2),
@ -44,8 +45,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED(18), HANDS_PLAYED(18),
LOCATIONS_PLAYED(19), LOCATIONS_PLAYED(19),
LONGEST_STREAKS(20), LONGEST_STREAKS(20),
MAXIMUM_NETRESULT(21), MAXIMUM_NET_RESULT(21),
MINIMUM_NETRESULT(22), MINIMUM_NET_RESULT(22),
MAXIMUM_DURATION(23), MAXIMUM_DURATION(23),
DAYS_PLAYED(24), DAYS_PLAYED(24),
WINNING_SESSION_COUNT(25), WINNING_SESSION_COUNT(25),
@ -53,6 +54,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
TOTAL_BUYIN(27), TOTAL_BUYIN(27),
RISK_OF_RUIN(28), RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29), STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31)
; ;
companion object : IntSearchable<Stat> { companion object : IntSearchable<Stat> {
@ -100,6 +103,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
} }
override val value: Int = this.uniqueIdentifier
override val resId: Int? override val resId: Int?
get() { get() {
return when (this) { return when (this) {
@ -124,11 +129,13 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED -> R.string.number_of_hands HANDS_PLAYED -> R.string.number_of_hands
LOCATIONS_PLAYED -> R.string.locations_played LOCATIONS_PLAYED -> R.string.locations_played
LONGEST_STREAKS -> R.string.longest_streaks LONGEST_STREAKS -> R.string.longest_streaks
MAXIMUM_NETRESULT -> R.string.max_net_result MAXIMUM_NET_RESULT -> R.string.max_net_result
MINIMUM_NETRESULT -> R.string.min_net_result MINIMUM_NET_RESULT -> R.string.min_net_result
MAXIMUM_DURATION -> R.string.longest_session MAXIMUM_DURATION -> R.string.longest_session
DAYS_PLAYED -> R.string.days_played DAYS_PLAYED -> R.string.days_played
TOTAL_BUYIN -> R.string.total_buyin TOTAL_BUYIN -> R.string.total_buyin
TOURNAMENT_ITM_RATIO -> R.string.itm_ratio
TOTAL_TIPS -> R.string.total_tips
else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined") else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined")
} }
} }
@ -145,7 +152,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
when (this) { when (this) {
// Amounts + red/green // Amounts + red/green
NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NETRESULT, MINIMUM_NETRESULT -> { NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NET_RESULT, MINIMUM_NET_RESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.toCurrency(currency), color) return TextFormat(value.toCurrency(currency), color)
} }
@ -161,7 +168,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> { HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
return TextFormat(value.formattedHourlyDuration()) return TextFormat(value.formattedHourlyDuration())
} // red/green percentages } // red/green percentages
WIN_RATIO, ROI -> { WIN_RATIO, ROI, TOURNAMENT_ITM_RATIO -> {
val color = if (value * 100 >= this.threshold) R.color.green else R.color.red val color = if (value * 100 >= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color) return TextFormat("${(value * 100).formatted}%", color)
} }
@ -169,9 +176,9 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val color = if (value * 100 <= this.threshold) R.color.green else R.color.red val color = if (value * 100 <= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color) return TextFormat("${(value * 100).formatted}%", color)
} }
// white amountsr // white amounts
AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN -> { STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN, TOTAL_TIPS -> {
return TextFormat(value.toCurrency(currency)) return TextFormat(value.toCurrency(currency))
} }
LONGEST_STREAKS -> { LONGEST_STREAKS -> {
@ -185,6 +192,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() { get() {
return when (this) { return when (this) {
RISK_OF_RUIN -> 5.0 RISK_OF_RUIN -> 5.0
TOURNAMENT_ITM_RATIO -> 10.0
WIN_RATIO -> 50.0 WIN_RATIO -> 50.0
else -> 0.0 else -> 0.0
} }
@ -200,12 +208,12 @@ 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
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context) WIN_RATIO, HOURLY_DURATION, TOURNAMENT_ITM_RATIO -> return this.localizedTitle(context)
else -> null else -> null
} }
resId?.let { resId?.let {
@ -239,8 +247,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() { get() {
return when (this) { return when (this) {
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB, HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, HANDS_PLAYED,
STANDARD_DEVIATION_BB_PER_100_HANDS -> false STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_TIPS -> false
else -> true else -> true
} }
} }
@ -257,7 +265,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val legendHideRightValue: Boolean val legendHideRightValue: Boolean
get() { get() {
return when (this) { return when (this) {
AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, WIN_RATIO, AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, WIN_RATIO, TOURNAMENT_ITM_RATIO,
HOURLY_DURATION, AVERAGE_HOURLY_DURATION -> true HOURLY_DURATION, AVERAGE_HOURLY_DURATION -> true
else -> false else -> false
} }
@ -269,7 +277,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val graphSignificantIndividualValue: Boolean val graphSignificantIndividualValue: Boolean
get() { get() {
return when (this) { return when (this) {
AVERAGE, WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES, AVERAGE, WIN_RATIO, TOURNAMENT_ITM_RATIO,
NUMBER_OF_SETS, NUMBER_OF_GAMES,
STANDARD_DEVIATION, HOURLY_DURATION -> false STANDARD_DEVIATION, HOURLY_DURATION -> false
else -> true else -> true
} }
@ -315,7 +324,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return when (this) { return when (this) {
NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB, NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
AVERAGE_HOURLY_DURATION, HOURLY_DURATION, AVERAGE_HOURLY_DURATION, HOURLY_DURATION,
NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, WIN_RATIO, NUMBER_OF_SETS, ROI, AVERAGE_BUYIN,
WIN_RATIO, TOURNAMENT_ITM_RATIO,
AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true
else -> false else -> false
} }

@ -39,10 +39,28 @@ class BankrollCalculator {
initialValue += bankroll.initialValue * rate initialValue += bankroll.initialValue * rate
} }
bankroll.transactions?.let { transactions -> if (setup.virtualBankroll) {
val sum = transactions.sum("amount") val sum = realm.where(Transaction::class.java)
.equalTo("bankroll.id", bankroll.id)
.notEqualTo("type.kind", TransactionType.Value.TRANSFER.uniqueIdentifier)
.sum("amount") // we remove transfer as they don't impact the overall bankroll
transactionNet += rate * sum.toDouble() transactionNet += rate * sum.toDouble()
} else {
bankroll.transactions?.let { transactions ->
val sum = transactions.sum("amount")
transactionNet += rate * sum.toDouble()
}
bankroll.destinationTransactions?.let { transactions ->
for (transaction in transactions) {
val transferRate = transaction.transferRate ?: 1.0
transactionNet += transferRate * transaction.amount * -1
}
}
} }
} }
report.transactionsNet = transactionNet report.transactionsNet = transactionNet
@ -71,20 +89,18 @@ class BankrollCalculator {
this.computeRiskOfRuin(report, result) this.computeRiskOfRuin(report, result)
} else { } else {
val results = Filter.queryOn<Result>(realm, baseQuery) val results = Filter.queryOn<Result>(realm, baseQuery)
report.netResult = results.sum("net").toDouble() report.netResult = results.sum("net").toDouble()
} }
val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
report.transactionBuckets[depositType.id]?.let { bucket -> report.transactionBuckets[depositType.id]?.let { bucket ->
report.depositTotal = bucket.transactions.sumByDouble { it.amount } report.depositTotal = bucket.transactions.sumOf { it.amount }
} }
val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
report.transactionBuckets[withdrawalType.id]?.let { bucket -> report.transactionBuckets[withdrawalType.id]?.let { bucket ->
report.withdrawalTotal = bucket.transactions.sumByDouble { it.amount } report.withdrawalTotal = bucket.transactions.sumOf { it.amount }
} }
report.generateGraphPointsIfNecessary() report.generateGraphPointsIfNecessary()

@ -13,7 +13,6 @@ import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.DataSetFactory import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import java.util.* import java.util.*
import kotlin.collections.HashMap
/** /**
* This class holds the results from the BankrollCalculator computations * This class holds the results from the BankrollCalculator computations
@ -64,6 +63,7 @@ class BankrollReport(var setup: BankrollReportSetup) {
*/ */
private fun computeBankrollTotal() { private fun computeBankrollTotal() {
this.total = this.initial + this.netResult + this.transactionsNet this.total = this.initial + this.netResult + this.transactionsNet
// Timber.d("init = $initial, net = $netResult, trans = $transactionsNet")
} }
/** /**
@ -220,14 +220,10 @@ class BankrollReportSetup(val bankrollId: String? = null, val from: Date? = null
query.add(bankrollCondition) query.add(bankrollCondition)
} }
this.from?.let { this.from?.let {
val fromCondition = QueryCondition.StartedFromDate() query.add(QueryCondition.StartedFromDate(it))
fromCondition.singleValue = it
query.add(fromCondition)
} }
this.to?.let { this.to?.let {
val toCondition = QueryCondition.StartedToDate() query.add(QueryCondition.StartedToDate(it))
toCondition.singleValue = it
query.add(toCondition)
} }
return query return query
} }

@ -0,0 +1,12 @@
package net.pokeranalytics.android.calculus.calcul
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.ui.graph.Graph
val AggregationType.axisFormatting: Graph.AxisFormatting
get() {
return when (this) {
AggregationType.DURATION -> Graph.AxisFormatting.X_DURATION
else -> Graph.AxisFormatting.DEFAULT
}
}

@ -0,0 +1,84 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import com.github.mikephil.charting.data.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Point
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import kotlin.math.abs
// MPAndroidChart
fun ComputedResults.defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun ComputedResults.singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun ComputedResults.durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this.evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this.evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}

@ -0,0 +1,64 @@
package net.pokeranalytics.android.calculus.calcul
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
/**
* The way the computed stats are going to be displayed
*/
enum class ReportDisplay : RowRepresentable {
TABLE,
PROGRESS,
COMPARISON,
MAP;
override val resId: Int?
get() {
return when (this) {
TABLE -> R.string.table
PROGRESS -> R.string.progress
COMPARISON -> R.string.comparison
MAP -> R.string.map
}
}
val activityClass: Class<*>
get() {
return when (this) {
TABLE -> TableReportActivity::class.java
PROGRESS -> ProgressReportActivity::class.java
COMPARISON -> ComparisonReportActivity::class.java
else -> throw PAIllegalStateException("undefined activity for report display")
}
}
val progressValues: Calculator.Options.ProgressValues
get() {
return when (this) {
PROGRESS, COMPARISON -> Calculator.Options.ProgressValues.STANDARD
else -> Calculator.Options.ProgressValues.NONE
}
}
// val requireProgressValues: Boolean
// get() {
// return when (this) {
// PROGRESS, COMPARISON -> true
// else -> false
// }
// }
val multipleStatSelection: Boolean
get() {
return when (this) {
PROGRESS -> false
else -> true
}
}
}

@ -0,0 +1,64 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.ColorUtils
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun Report.lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: this.options.stats.firstOrNull()
val statName = statToUse?.name ?: ""
statToUse?.let {
this.results.forEachIndexed { index, results ->
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun Report.barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.firstOrNull()
statToUse?.let {
this.results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
}
}
}
val label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun Report.multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this.results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}

@ -0,0 +1,81 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
class StatRepresentable(var stat: Stat) : RowRepresentable {
companion object {
fun resId(stat: Stat): Int {
return when (stat) {
Stat.NET_RESULT -> R.string.net_result
Stat.BB_NET_RESULT -> R.string.total_net_result_bb_
Stat.HOURLY_RATE -> R.string.average_hour_rate
Stat.AVERAGE -> R.string.average
Stat.NUMBER_OF_SETS -> R.string.number_of_sessions
Stat.NUMBER_OF_GAMES -> R.string.number_of_records
Stat.HOURLY_DURATION -> R.string.duration
Stat.AVERAGE_HOURLY_DURATION -> R.string.average_hours_played
Stat.NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
Stat.HOURLY_RATE_BB -> R.string.average_hour_rate_bb_
Stat.AVERAGE_NET_BB -> R.string.average_net_result_bb_
Stat.WIN_RATIO -> R.string.win_ratio
Stat.AVERAGE_BUYIN -> R.string.average_buyin
Stat.ROI -> R.string.tournament_roi
Stat.STANDARD_DEVIATION -> R.string.standard_deviation
Stat.STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
Stat.STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation
Stat.HANDS_PLAYED -> R.string.number_of_hands
Stat.LOCATIONS_PLAYED -> R.string.locations_played
Stat.LONGEST_STREAKS -> R.string.longest_streaks
Stat.MAXIMUM_NET_RESULT -> R.string.max_net_result
Stat.MINIMUM_NET_RESULT -> R.string.min_net_result
Stat.MAXIMUM_DURATION -> R.string.longest_session
Stat.DAYS_PLAYED -> R.string.days_played
Stat.TOTAL_BUYIN -> R.string.total_buyin
else -> throw PAIllegalStateException("Stat ${stat.name} name required but undefined")
}
}
fun localizedTitle(stat: Stat, context: Context): String {
return context.getString(resId(stat))
}
}
override val resId: Int
get() {
return resId(this.stat)
}
/**
* Returns a label used to display the legend right value, typically a total or an average
*/
fun cumulativeLabelResId(context: Context): String {
val resId = when (this.stat) {
Stat.AVERAGE, Stat.AVERAGE_HOURLY_DURATION, Stat.NET_BB_PER_100_HANDS,
Stat.HOURLY_RATE_BB, Stat.AVERAGE_NET_BB, Stat.ROI, Stat.HOURLY_RATE -> R.string.average
Stat.NUMBER_OF_SETS -> R.string.number_of_sessions
Stat.NUMBER_OF_GAMES -> R.string.number_of_records
Stat.NET_RESULT -> R.string.total
Stat.STANDARD_DEVIATION -> R.string.net_result
Stat.STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
Stat.STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
Stat.WIN_RATIO, Stat.HOURLY_DURATION -> return this.localizedTitle(context)
else -> null
}
resId?.let {
return context.getString(it)
} ?: run {
return NULL_TEXT
}
}
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}

@ -21,150 +21,163 @@ import kotlin.math.round
*/ */
class CashGameOptimalDurationCalculator { class CashGameOptimalDurationCalculator {
companion object { companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval = 4 // number of duration tests inside the bucket to find the best duration private const val bucketInterval =
private const val minimumValidityCount = 10 // the number of sessions inside a bucket to start having a reasonable average 4 // number of duration tests inside the bucket to find the best duration
private const val intervalValidity = 3 // the minimum number of unit between the shortest & longest valid buckets private const val minimumValidityCount =
private const val polynomialDegree = 7 // the degree of the computed polynomial 10 // the number of sessions inside a bucket to start having a reasonable average
private const val intervalValidity =
/*** 3 // the minimum number of unit between the shortest & longest valid buckets
* Starts the calculation private const val polynomialDegree = 7 // the degree of the computed polynomial
* [isLive] is a boolean to indicate if we're looking at live or online games
* return a duration or null if it could not be computed /***
*/ * Starts the calculation
fun start(isLive: Boolean): Double? { * [isLive] is a boolean to indicate if we're looking at live or online games
* return a duration or null if it could not be computed
val realm = Realm.getDefaultInstance() */
fun start(isLive: Boolean): Double? {
val query = Query().add(QueryCondition.IsCash) // cash game
query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online val realm = Realm.getDefaultInstance()
query.add(QueryCondition.EndDateNotNull) // ended
query.add(QueryCondition.BigBlindNotNull) // has BB value val query = Query().add(QueryCondition.IsCash) // cash game
query.add(
val sessions = query.queryWith(realm.where(Session::class.java)).findAll() if (isLive) {
val sessionsByDuration = sessions.groupBy { QueryCondition.IsLive
val dur = round((it.netDuration / bucket).toDouble()) * bucket } else {
Timber.d("Stop notif > key: $dur") QueryCondition.IsOnline
dur }
} ) // live / online
query.add(QueryCondition.EndDateNotNull) // ended
// define validity interval query.add(QueryCondition.BiggestBetNotNull) // has BB value
var start: Double? = null
var end: Double? = null val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
var validBuckets = 0 val sessionsByDuration = sessions.groupBy {
val dur = round((it.netDuration / bucket).toDouble()) * bucket
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() // Timber.d("Stop notif > key: $dur")
Timber.d("Stop notif > keys: $hkeys ") dur
for (key in sessionsByDuration.keys.sorted()) { }
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) { // define validity interval
start = key var start: Double? = null
} var end: Double? = null
if (sessionCount >= minimumValidityCount) { var validBuckets = 0
end = key
validBuckets++ val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
} // Timber.d("Stop notif > keys: $hkeys ")
} for (key in sessionsByDuration.keys.sorted()) {
Timber.d("Stop notif > validBuckets: $validBuckets ") val sessionCount = sessionsByDuration[key]?.size ?: 0
if (!(start != null && end != null && (end - start) >= intervalValidity)) { if (start == null && sessionCount >= minimumValidityCount) {
Timber.d("Stop notif > invalid setup: $start / $end ") start = key
return null }
} if (sessionCount >= minimumValidityCount) {
end = key
// define if we have enough sessions validBuckets++
if (sessions.size < 50) { }
Timber.d("Stop notif > not enough sessions: ${sessions.size} ") }
return null // Timber.d("Stop notif > validBuckets: $validBuckets ")
} if (!(start != null && end != null && (end - start) >= intervalValidity)) {
// Timber.d("Stop notif > invalid setup: $start / $end ")
val options = Calculator.Options() return null
options.query = query }
val report = Calculator.computeStats(realm, options)
val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value // define if we have enough sessions
if (sessions.size < 50) {
val p = polynomialRegression(sessions, stdBB) // Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
var bestAverage = 0.0 }
var bestHourlyRate = 0.0
var bestDuration = 0.0 val options = Calculator.Options()
var maxDuration = 0.0 options.query = query
val report = Calculator.computeStats(realm, options)
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted() val stdBB =
report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
for (key in keys) {
val p = polynomialRegression(sessions, stdBB)
val sessionCount = sessionsByDuration[key]?.size ?: 0
var bestAverage = 0.0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid var bestHourlyRate = 0.0
var bestDuration = 0.0
for (i in 0 until bucketInterval) { var maxDuration = 0.0
val duration = key + i * bucket / bucketInterval val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
val averageResult = getBB(duration, p) for (key in keys) {
val hourly = averageResult / duration
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { val sessionCount = sessionsByDuration[key]?.size ?: 0
bestAverage = averageResult
bestDuration = duration if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
}
for (i in 0 until bucketInterval) {
if (duration > 0 && hourly > bestHourlyRate) {
bestHourlyRate = hourly val duration = key + i * bucket / bucketInterval
}
if (duration > maxDuration){ val averageResult = getBB(duration, p)
maxDuration = duration val hourly = averageResult / duration
} if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) {
} bestAverage = averageResult
bestDuration = duration
} }
if (bestDuration > 0.0) { if (duration > 0 && hourly > bestHourlyRate) {
return bestDuration bestHourlyRate = hourly
} }
if (duration > maxDuration) {
Timber.d("Stop notif > not found, best duration: $bestDuration") maxDuration = duration
realm.close() }
return null }
}
}
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
var y = 0.0 if (bestDuration > 0.0) {
for (i in polynomial.indices) { return bestDuration
y += polynomial[i] * netDuration.pow(i) }
}
return y // Timber.d("Stop notif > not found, best duration: $bestDuration")
} realm.close()
return null
private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray { }
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
var y = 0.0
val points = WeightedObservedPoints() for (i in polynomial.indices) {
val now = Date().time y += polynomial[i] * netDuration.pow(i)
}
sessions.forEach { return y
var weight = 5.0 }
val endTime = it.endDate?.time ?: 0L private fun polynomialRegression(
sessions: List<Session>,
val age = now - endTime bbStandardDeviation: Double?
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point ): DoubleArray {
weight -= 1.0
} val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points
weight -= 3.0 val points = WeightedObservedPoints()
} val now = Date().time
points.add(weight, it.netDuration.toDouble(), it.bbNet) sessions.forEach {
var weight = 5.0
}
val endTime = it.endDate?.time ?: 0L
val age = now - endTime
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point
weight -= 1.0
}
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points
weight -= 3.0
}
points.add(weight, it.netDuration.toDouble(), it.bbNet)
}
// polynomial of 7 degree, same as iOS // polynomial of 7 degree, same as iOS
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList()) return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
} }
} }
} }

@ -1,6 +1,6 @@
package net.pokeranalytics.android.exceptions package net.pokeranalytics.android.exceptions
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow import net.pokeranalytics.android.model.Criteria
class ModelException(message: String) : Exception(message) class ModelException(message: String) : Exception(message)
class FormattingException(message: String) : Exception(message) class FormattingException(message: String) : Exception(message)
@ -14,16 +14,19 @@ class PAIllegalStateException(message: String) : Exception(message)
sealed class PokerAnalyticsException(message: String) : Exception(message) { sealed class PokerAnalyticsException(message: String) : Exception(message) {
object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition") object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition")
object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition") // object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition")
object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized") // object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized")
object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable") // object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing") class QueryValueMapUnknown(message: String = "fieldName is missing"): PokerAnalyticsException(message)
object QueryTypeUnhandled: PokerAnalyticsException(message = "queryWith type not handled") class QueryTypeUnhandled(clazz: String) :
PokerAnalyticsException(message = "queryWith type not handled: $clazz")
class ComparisonCriteriaUnhandled(criteria: Criteria) :
PokerAnalyticsException(message = "Criteria type not handled: ${criteria.uniqueIdentifier}")
object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected") object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected")
object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null") object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null")
data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "queryWith element '$filterElementRow' type is missing") // data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "queryWith element '$filterElementRow' type is missing")
data class QueryValueMapMissingKeys(val missingKeys: List<String>) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys") // data class QueryValueMapMissingKeys(val missingKeys: List<String>) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys")
data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow") // data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow")
data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}") // data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment") // object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment")
} }

@ -7,10 +7,10 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison
import net.pokeranalytics.android.model.Criteria.Blinds.comparison
import net.pokeranalytics.android.model.Criteria.Games.comparison import net.pokeranalytics.android.model.Criteria.Games.comparison
import net.pokeranalytics.android.model.Criteria.Limits.comparison import net.pokeranalytics.android.model.Criteria.Limits.comparison
import net.pokeranalytics.android.model.Criteria.Locations.comparison import net.pokeranalytics.android.model.Criteria.Locations.comparison
import net.pokeranalytics.android.model.Criteria.Stakes.comparison
import net.pokeranalytics.android.model.Criteria.TableSizes.comparison import net.pokeranalytics.android.model.Criteria.TableSizes.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison
@ -68,7 +68,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach { realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
objects.add(QueryCondition.CustomFieldListQuery(it)) objects.add(QueryCondition.CustomFieldListQuery(it))
} }
objects.sorted() objects.sort()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -104,11 +104,10 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
} }
objects.add(condition) objects.add(condition)
} }
objects.sorted() objects.sort()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
QueryCondition.distinct<Session, T, S>()?.let { QueryCondition.distinct<Session, T, S>()?.let {
val values = it.mapNotNull { session -> val values = it.mapNotNull { session ->
when (this) { when (this) {
@ -124,8 +123,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is TournamentFees -> if (session.tournamentEntryFee is S) { is TournamentFees -> if (session.tournamentEntryFee is S) {
session.tournamentEntryFee as S session.tournamentEntryFee as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is Blinds -> if (session.blinds is S) { is Stakes -> if (session.cgStakes is S) {
session.blinds as S session.cgStakes as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
else -> null else -> null
} }
@ -159,12 +158,13 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14) object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14)
object Years : ListCriteria(15) object Years : ListCriteria(15)
object AllMonthsUpToNow : ListCriteria(16) object AllMonthsUpToNow : ListCriteria(16)
object Blinds : ListCriteria(17) object Stakes : ListCriteria(17)
object TournamentFees : ListCriteria(18) object TournamentFees : ListCriteria(18)
object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19) object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20) object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
val queries: List<Query> val queries: List<Query>
get() { get() {
@ -237,19 +237,34 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.close() realm.close()
years years
} }
is Blinds -> comparison<QueryCondition.AnyBlind, String>() is Stakes -> comparison<QueryCondition.AnyStake, String>()
is ListCustomFields -> comparison<CustomFieldEntry>() is ListCustomFields -> comparison<CustomFieldEntry>()
is ValueCustomFields -> { is ValueCustomFields -> {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val queries = when (this.customFieldType(realm)) { val queries = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >() CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >() CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
else -> throw PokerAnalyticsException.QueryTypeUnhandled else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
} }
realm.close() realm.close()
queries queries
} }
else -> throw PokerAnalyticsException.QueryTypeUnhandled is Duration -> {
val hourlyQueries = (0..12).map { i ->
val more = QueryCondition.Duration(i * 60)
more.operator = QueryCondition.Operator.MORE_OR_EQUAL
val less = QueryCondition.Duration((i + 1) * 60)
less.operator = QueryCondition.Operator.LESS
Query(more, less)
}.toMutableList()
val moreThan12Hours = QueryCondition.Duration(12 * 60)
moreThan12Hours.operator = QueryCondition.Operator.MORE_OR_EQUAL
hourlyQueries.add(Query(moreThan12Hours))
hourlyQueries
}
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
} }
} }
@ -271,7 +286,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
DayPeriods -> R.string.weekdays_or_weekend DayPeriods -> R.string.weekdays_or_weekend
Years -> R.string.year Years -> R.string.year
AllMonthsUpToNow -> R.string.month AllMonthsUpToNow -> R.string.month
Blinds -> R.string.blind Stakes -> R.string.blind
TournamentFees -> R.string.entry_fees TournamentFees -> R.string.entry_fees
// is ListCustomFields -> this.customField.resId // is ListCustomFields -> this.customField.resId
// is ValueCustomFields -> this.customField.resId // is ValueCustomFields -> this.customField.resId
@ -284,13 +299,13 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> { inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> {
val objects = mutableListOf<S>() val objects = mutableListOf<S>()
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.where<T>().findAll().forEach { realm.where<T>().sort("name").findAll().forEach {
val condition = (QueryCondition.getInstance<T>() as S).apply { val condition = (QueryCondition.getInstance<T>() as S).apply {
setObject(it) setObject(it)
} }
objects.add(condition) objects.add(condition)
} }
objects.sorted() // objects.sort()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -303,7 +318,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
} }
objects.add(condition) objects.add(condition)
} }
objects.sorted() objects.sort()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -319,7 +334,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
TournamentFeatures, Limits, TableSizes, TournamentTypes, TournamentFeatures, Limits, TableSizes, TournamentTypes,
MonthsOfYear, DaysOfWeek, SessionTypes, MonthsOfYear, DaysOfWeek, SessionTypes,
BankrollTypes, DayPeriods, Years, BankrollTypes, DayPeriods, Years,
AllMonthsUpToNow, Blinds, TournamentFees AllMonthsUpToNow,
Stakes, TournamentFees
) )
} }
} }

@ -0,0 +1,38 @@
package net.pokeranalytics.android.model
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.PerformanceKey
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
enum class LiveOnline(override var uniqueIdentifier: Int) : PerformanceKey, IntIdentifiable {
LIVE(0),
ONLINE(1);
companion object : IntSearchable<LiveOnline> {
override fun valuesInternal(): Array<LiveOnline> {
return values()
}
}
override val resId: Int?
get() {
return when (this) {
LIVE -> R.string.live
ONLINE -> R.string.online
}
}
override val value: Int
get() {
return this.uniqueIdentifier
}
val isLive: Boolean
get() {
return (this == LIVE)
}
}

@ -0,0 +1,13 @@
package net.pokeranalytics.android.model
data class Stakes(var blinds: String?, var ante: Double?) {
companion object {
}
}

@ -0,0 +1,13 @@
package net.pokeranalytics.android.model.blogpost
import com.google.gson.annotations.SerializedName
class BlogPost {
@SerializedName("id")
var id: Int = 0
@SerializedName("level")
var content: String = ""
}

@ -2,10 +2,10 @@ 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
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.NotificationSchedule import net.pokeranalytics.android.util.NotificationSchedule
@ -36,17 +36,17 @@ enum class SessionState {
fun Session.getState(): SessionState { fun Session.getState(): SessionState {
val start = this.startDate val start = this.startDate
if (start == null) { return if (start == null) {
return SessionState.PENDING SessionState.PENDING
} else { } else {
if (start > Date()) { if (start > Date()) {
return SessionState.PLANNED SessionState.PLANNED
} else if (this.endDate != null) { } else if (this.endDate != null) {
return SessionState.FINISHED SessionState.FINISHED
} else if (this.pauseDate != null) { } else if (this.pauseDate != null) {
return SessionState.PAUSED SessionState.PAUSED
} else { } else {
return SessionState.STARTED SessionState.STARTED
} }
} }
} }
@ -76,8 +76,8 @@ fun Session.getFormattedGameType(context: Context): String {
parameters.add(context.getString(R.string.tournament).capitalize()) parameters.add(context.getString(R.string.tournament).capitalize())
} }
} else { } else {
if (cgSmallBlind != null && cgBigBlind != null) { if (this.cgAnte != null || this.cgBlinds != null) {
parameters.add(getFormattedBlinds()) parameters.add(getFormattedStakes())
} }
game?.let { game?.let {
parameters.add(getFormattedGame()) parameters.add(getFormattedGame())
@ -103,7 +103,9 @@ fun Session.cancelStopNotification(context: Context) {
fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) { fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
val timeDelay = optimalDuration - this.currentDuration() val timeDelay = optimalDuration - this.currentDuration()
if (timeDelay <= 0) throw PAIllegalStateException("A stop notif has been setup for the past: start=${this.startDate}, end=${this.endDate}, optimalDuration=$optimalDuration") if (timeDelay <= 0) {
return
}
val title = context.getString(R.string.stop_notification_title) val title = context.getString(R.string.stop_notification_title)
val body = context.getString(R.string.stop_notification_body) val body = context.getString(R.string.stop_notification_body)
@ -118,11 +120,10 @@ 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)
} }
val AbstractList<Session>.hourlyDuration: Double val AbstractList<Session>.hourlyDuration: Double
get() { get() {
val intervals = mutableListOf<TimeInterval>() val intervals = mutableListOf<TimeInterval>()
@ -130,7 +131,7 @@ val AbstractList<Session>.hourlyDuration: Double
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration) val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval) intervals.update(interval)
} }
return intervals.sumByDouble { it.hourlyDuration } return intervals.sumOf { it.hourlyDuration }
} }
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) { class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {

@ -1,10 +1,10 @@
package net.pokeranalytics.android.model.filter package net.pokeranalytics.android.model.filter
import com.crashlytics.android.Crashlytics
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.CrashLogging
/** /**
* We want to be able to store filters in the database: * We want to be able to store filters in the database:
@ -65,7 +65,7 @@ class FilterHelper {
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition) Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition) Result::class.java -> Result.fieldNameForQueryType(queryCondition)
else -> { else -> {
Crashlytics.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}")) CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null null
// throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}") // throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}")
} }

@ -11,9 +11,13 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query { class Query {
constructor(vararg elements: QueryCondition) { constructor(query: Query) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
this.add(elements.asList()) this.add(elements.filterNotNull())
} }
} }
@ -35,7 +39,7 @@ class Query {
return this return this
} }
fun add(queryConditions: List<QueryCondition>): Query{ fun add(queryConditions: List<QueryCondition>): Query {
this._conditions.addAll(queryConditions) this._conditions.addAll(queryConditions)
return this return this
} }
@ -52,10 +56,10 @@ class Query {
} }
} }
fun getName(context: Context): String { fun getName(context: Context, separator: String = " + "): String {
return when (this._conditions.size) { return when (this._conditions.size) {
0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction... 0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction...
else -> this._conditions.joinToString(" + ") { it.getDisplayName(context) } else -> this._conditions.joinToString(separator) { it.getDisplayNameWithValues(context) }
} }
} }
@ -84,14 +88,14 @@ class Query {
} }
// println("<<<<<< ${realmQuery.description}") // println("<<<<<< ${realmQuery.description}")
val queryLast = this.conditions.firstOrNull { // val queryLast = this.conditions.firstOrNull {
it is QueryCondition.Last // it is QueryCondition.Last
} // }
queryLast?.let {qc -> // queryLast?.let {qc ->
(qc as QueryCondition.Last).singleValue?.let { // (qc as QueryCondition.Last).singleValue?.let {
return realmQuery.limit(it.toLong()) // return realmQuery.limit(it.toLong())
} // }
} // }
return realmQuery return realmQuery
} }
@ -100,4 +104,25 @@ class Query {
return this return this
} }
fun copy(): Query {
return Query(this)
}
/*
Returns the first object Id of any QueryCondition
*/
val objectId: String?
get() {
for (c in this._conditions) {
when (c) {
is QueryCondition.QueryDataCondition<*> -> {
c.objectId?.let { return it }
}
else -> {}
}
}
return null
}
} }

@ -12,45 +12,73 @@ import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.interfaces.CodedStake
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.StakesHolder
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.reflect.KClass
/** /**
* Enum describing the way a query should be handled * Enum describing the way a query should be handled
* Some queries requires a value to be checked upon through equals, in, more, less, between * Some queries requires a value to be checked upon through equals, in, more, less, between
*/ */
sealed class QueryCondition : RowRepresentable {
sealed class QueryCondition : FilterElementRow {
companion object { companion object {
inline fun <reified T : QueryCondition> more(): T {
return T::class.java.newInstance().apply { this.operator = Operator.MORE } fun <T: Any> newInstance(kClass: KClass<T>): T {
return try {
kClass.objectInstance ?: kClass.java.newInstance()
} catch (e: Exception) {
// some object instance can fail due to: java.lang.Class has no zero argument constructor
// We should have just one constructor with a single parameter
val primaryConstructor = kClass.java.declaredConstructors.first()
val paramClass = primaryConstructor.parameterTypes.first()
val param = when (paramClass) {
Int::class.java -> 0
Double::class.java -> 0.0
else -> paramClass.newInstance()
}
val constructor = kClass.java.getDeclaredConstructor(paramClass)
constructor.newInstance(param)
}
} }
inline fun <reified T : QueryCondition> less(): T { // inline fun <reified T : QueryCondition> more(): T {
return T::class.java.newInstance().apply { this.operator = Operator.LESS } // return newInstance(T::class).apply { this.operator = Operator.MORE }
// }
//
// inline fun <reified T : QueryCondition> less(): T {
// return newInstance(T::class).apply { this.operator = Operator.LESS }
// }
inline fun <reified T : QueryCondition> moreOrEqual(): T {
return newInstance(T::class).apply { this.operator = Operator.MORE_OR_EQUAL }
} }
inline fun <reified T : QueryCondition> moreOrLess(): ArrayList<T> { inline fun <reified T : QueryCondition> lessOrEqual(): T {
return arrayListOf(more(), less()) return newInstance(T::class).apply { this.operator = Operator.LESS_OR_EQUAL }
}
inline fun <reified T : QueryCondition> moreEqualOrLessEqual(): ArrayList<T> {
return arrayListOf(moreOrEqual(), lessOrEqual())
} }
fun <T : QueryCondition> valueOf(name: String): T { fun <T : QueryCondition> valueOf(name: String): T {
val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin
val instance = kClass.objectInstance ?: kClass.java.newInstance() return newInstance(kClass) as T
return instance as T
} }
inline fun <reified T : Identifiable> getInstance(): QueryCondition { inline fun <reified T : Identifiable> getInstance(): QueryCondition {
@ -61,13 +89,14 @@ sealed class QueryCondition : FilterElementRow {
TransactionType::class.java -> AnyTransactionType() TransactionType::class.java -> AnyTransactionType()
TournamentName::class.java -> AnyTournamentName() TournamentName::class.java -> AnyTournamentName()
TournamentFeature::class.java -> AllTournamentFeature() TournamentFeature::class.java -> AllTournamentFeature()
else -> throw PokerAnalyticsException.QueryTypeUnhandled else -> throw PokerAnalyticsException.QueryTypeUnhandled((T::class.java).name)
} }
} }
inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? { inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let { FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh()
val distincts = when (T::class) { val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING) String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
@ -85,7 +114,9 @@ sealed class QueryCondition : FilterElementRow {
ANY, ANY,
ALL, ALL,
MORE, MORE,
MORE_OR_EQUAL,
LESS, LESS,
LESS_OR_EQUAL,
EQUALS, EQUALS,
TRUE, TRUE,
NOTNULL NOTNULL
@ -97,7 +128,7 @@ sealed class QueryCondition : FilterElementRow {
val groupId: String val groupId: String
get() { get() {
return when (this.operator) { return when (this.operator) {
Operator.MORE, Operator.LESS -> return "${this.operator.name.toLowerCase().capitalize()}$baseId" Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return "${this.operator.name.toLowerCase().capitalize()}$baseId"
else -> this.baseId else -> this.baseId
} }
} }
@ -105,7 +136,7 @@ sealed class QueryCondition : FilterElementRow {
val id: List<String> val id: List<String>
get() { get() {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}") Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return listOf("$baseId+${this.operator.name}")
else -> {} else -> {}
} }
@ -123,24 +154,43 @@ sealed class QueryCondition : FilterElementRow {
abstract var operator: Operator abstract var operator: Operator
override fun getDisplayName(context: Context): String {
this.resId?.let {
return context.getString(it)
}
return baseId
}
open fun getDisplayNameWithValues(context: Context): String {
return getDisplayName(context)
}
abstract class ListOfValues<T> : QueryCondition(), Comparable<ListOfValues<T>> where T : Comparable<T> { abstract class ListOfValues<T> : QueryCondition(), Comparable<ListOfValues<T>> where T : Comparable<T> {
abstract var listOfValues: ArrayList<T> abstract var listOfValues: MutableList<T>
abstract fun labelForValue(value: T, context: Context): String abstract fun labelForValue(value: T, context: Context): String
open fun entityName(context: Context): String { open fun entityName(context: Context): String {
return getDisplayName(context) return getDisplayName(context)
} }
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.listOfValues)
}
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return getDisplayName(context, this.listOfValues)
}
private fun getDisplayName(context: Context, values: List<T>): String {
val prefix = this.resId?.let { val prefix = this.resId?.let {
context.getString(it) + " " context.getString(it) + " "
} ?: "" } ?: ""
return when (listOfValues.size) { return when (values.size) {
0 -> return NULL_TEXT 0 -> return NULL_TEXT
1, 2 -> prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ") else -> prefix + values.joinToString(", ") { labelForValue(it, context) }
else -> "${listOfValues.size} $prefix ${entityName(context)}" // else -> "${values.size} $prefix ${entityName(context)}"
} }
} }
@ -151,17 +201,41 @@ sealed class QueryCondition : FilterElementRow {
fun firstValue(context: Context): String? { fun firstValue(context: Context): String? {
return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) } return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) }
} }
} }
abstract class SingleValue<T> : ListOfValues<T>() where T : Comparable<T> { abstract class SingleValue<T>(value: T) : QueryCondition() where T : Comparable<T> {
override var listOfValues = ArrayList<T>()
abstract var singleValue: T? var singleValue: T = value
abstract fun labelForValue(value: T, context: Context): String
override fun getDisplayName(context: Context): String {
return this.resId?.let {
context.getString(it)
} ?: ""
// return getDisplayName(context, this.singleValue)
}
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.singleValue)
}
open fun getDisplayName(context: Context, value: T): String {
val prefix = this.resId?.let {
context.getString(it)
} ?: ""
return prefix + " " + labelForValue(value, context)
}
} }
abstract class ListOfDouble : ListOfValues<Double>() { abstract class ListOfDouble : ListOfValues<Double>() {
open var sign: Int = 1 open var sign: Int = 1
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues: ArrayList<Double> = arrayListOf() override var listOfValues = mutableListOf<Double>()
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues() listOfValues = filterCondition.getValues()
@ -174,7 +248,7 @@ sealed class QueryCondition : FilterElementRow {
abstract class ListOfInt : ListOfValues<Int>() { abstract class ListOfInt : ListOfValues<Int>() {
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues: ArrayList<Int> = arrayListOf() override var listOfValues = mutableListOf<Int>()
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues() listOfValues = filterCondition.getValues()
@ -187,7 +261,8 @@ sealed class QueryCondition : FilterElementRow {
abstract class ListOfString : ListOfValues<String>() { abstract class ListOfString : ListOfValues<String>() {
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues = ArrayList<String>() override var listOfValues = mutableListOf<String>()
override fun labelForValue(value: String, context: Context): String { override fun labelForValue(value: String, context: Context): String {
return value return value
} }
@ -198,56 +273,34 @@ sealed class QueryCondition : FilterElementRow {
} }
} }
abstract class SingleDate : SingleValue<Date>() { abstract class SingleDate(date: Date) : SingleValue<Date>(date) {
override fun labelForValue(value: Date, context: Context): String { override fun labelForValue(value: Date, context: Context): String {
return value.shortDate() return value.shortDate()
} }
override var listOfValues = ArrayList<Date>() // override var listOfValues = mutableListOf<Date>()
override var singleValue: Date?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue() singleValue = filterCondition.getValue()
} }
} }
abstract class SingleInt : SingleValue<Int>() { abstract class SingleInt(value: Int) : SingleValue<Int>(value) {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return value.toString() return value.toString()
} }
override var singleValue: Int?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue() singleValue = filterCondition.getValue()
} }
}
override fun getDisplayName(context: Context): String {
this.resId?.let {
return context.getString(it)
}
return baseId
} }
override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament // override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament
abstract class QueryDataCondition<T : NameManageable> : ListOfString() { abstract class QueryDataCondition<T : NameManageable> : ListOfString() {
fun setObject(dataObject: T) { fun setObject(dataObject: T) {
@ -263,7 +316,7 @@ sealed class QueryCondition : FilterElementRow {
val completeLabel = when (listOfValues.size) { val completeLabel = when (listOfValues.size) {
0 -> NULL_TEXT 0 -> NULL_TEXT
1, 2 -> { 1, 2 -> {
listOfValues.map { labelForValue(realm, it) }.joinToString(", ") listOfValues.joinToString(", ") { labelForValue(realm, it) }
} }
else -> "${listOfValues.size} $entityName" else -> "${listOfValues.size} $entityName"
} }
@ -271,6 +324,10 @@ sealed class QueryCondition : FilterElementRow {
return completeLabel return completeLabel
} }
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context)
}
open fun entityName(realm: Realm, context: Context): String { open fun entityName(realm: Realm, context: Context): String {
return entityName(context) return entityName(context)
} }
@ -279,28 +336,31 @@ sealed class QueryCondition : FilterElementRow {
val query = realm.where(entity) val query = realm.where(entity)
return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
} }
}
val objectId: String?
get() {
return this.listOfValues.firstOrNull()
}
}
interface DateTime { interface DateTime {
val showTime: Boolean val showTime: Boolean
} }
abstract class DateQuery : SingleDate(), DateTime { abstract class DateQuery(date: Date) : SingleDate(date), DateTime {
override val showTime: Boolean = false override val showTime: Boolean = false
override fun labelForValue(value: Date, context: Context): String { override fun labelForValue(value: Date, context: Context): String {
return singleValue?.let { return if (showTime) {
if (showTime) { singleValue.shortTime()
it.shortTime() } else {
} else { singleValue.shortDate()
it.shortDate() }
}
} ?: NULL_TEXT
} }
} }
abstract class TimeQuery : DateQuery() { abstract class TimeQuery(date: Date) : DateQuery(date) {
override val showTime: Boolean = true override val showTime: Boolean = true
} }
@ -343,50 +403,34 @@ sealed class QueryCondition : FilterElementRow {
} }
} }
class AnyTournamentName() : QueryDataCondition<TournamentName>() { class AnyTournamentName : QueryDataCondition<TournamentName>() {
override val entity: Class<TournamentName> = TournamentName::class.java override val entity: Class<TournamentName> = TournamentName::class.java
constructor(tournamentName: TournamentName) : this() {
this.setObject(tournamentName)
}
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
return context.getString(R.string.tournament_names) return context.getString(R.string.tournament_names)
} }
} }
class AnyTournamentFeature() : QueryDataCondition<TournamentFeature>() { class AnyTournamentFeature : QueryDataCondition<TournamentFeature>() {
override val entity: Class<TournamentFeature> = TournamentFeature::class.java override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature) : this() {
this.setObject(tournamentFeature)
}
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
return context.getString(R.string.tournament_features) return context.getString(R.string.tournament_features)
} }
} }
class AllTournamentFeature() : QueryDataCondition<TournamentFeature>() { class AllTournamentFeature : QueryDataCondition<TournamentFeature>() {
override var operator = Operator.ALL override var operator = Operator.ALL
override val entity: Class<TournamentFeature> = TournamentFeature::class.java override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature) : this() {
this.setObject(tournamentFeature)
}
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
return context.getString(R.string.tournament_features) return context.getString(R.string.tournament_features)
} }
} }
class AnyLocation() : QueryDataCondition<Location>() { class AnyLocation : QueryDataCondition<Location>() {
override val entity: Class<Location> = Location::class.java override val entity: Class<Location> = Location::class.java
constructor(location: Location) : this() {
this.setObject(location)
}
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
return context.getString(R.string.locations) return context.getString(R.string.locations)
} }
@ -434,19 +478,21 @@ sealed class QueryCondition : FilterElementRow {
} }
} }
class AnyBlind : ListOfString() { class AnyStake : ListOfString() {
override fun labelForValue(value: String, context: Context): String {
return StakesHolder.readableStakes(value)
}
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
return context.getString(R.string.blinds) return context.getString(R.string.stakes)
} }
}
object Last : SingleInt() { override fun compareTo(other: ListOfValues<String>): Int {
override var operator = Operator.EQUALS return CodedStake(this.listOfValues.first()).compareTo(CodedStake(other.listOfValues.first()))
override fun getDisplayName(context: Context): String {
//TODO update string "last %i"
return "${context.getString(R.string.last_i_records)} $singleValue"
} }
}
}
class NumberOfTable : ListOfInt() { class NumberOfTable : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
@ -476,13 +522,13 @@ sealed class QueryCondition : FilterElementRow {
} }
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
val suffix = when (value%10) { val suffix = when (value % 10) {
1 -> context.getString(R.string.ordinal_suffix_first) 1 -> context.getString(R.string.ordinal_suffix_first)
2 -> context.getString(R.string.ordinal_suffix_second) 2 -> context.getString(R.string.ordinal_suffix_second)
3 -> context.getString(R.string.ordinal_suffix_third) 3 -> context.getString(R.string.ordinal_suffix_third)
else -> context.getString(R.string.ordinal_suffix_default) else -> context.getString(R.string.ordinal_suffix_default)
} }
return "$value$suffix "+context.getString(R.string.position) return "$value$suffix " + context.getString(R.string.position)
} }
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
@ -499,7 +545,7 @@ sealed class QueryCondition : FilterElementRow {
class TournamentNumberOfPlayer : ListOfInt() { class TournamentNumberOfPlayer : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return value.toString() + " " + context.getString(R.string.number_of_players) return value.toString() + " " + context.getString(R.string.players).toLowerCase()
} }
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
@ -507,20 +553,20 @@ sealed class QueryCondition : FilterElementRow {
} }
} }
class StartedFromDate : DateQuery() { class StartedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE override var operator = Operator.MORE_OR_EQUAL
} }
class StartedToDate : DateQuery() { class StartedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS override var operator = Operator.LESS_OR_EQUAL
} }
class EndedFromDate : DateQuery() { class EndedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE override var operator = Operator.MORE_OR_EQUAL
} }
class EndedToDate : DateQuery() { class EndedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS override var operator = Operator.LESS_OR_EQUAL
} }
class AnyDayOfWeek : ListOfInt() { class AnyDayOfWeek : ListOfInt() {
@ -530,23 +576,23 @@ sealed class QueryCondition : FilterElementRow {
} }
class AnyMonthOfYear() : ListOfInt() { class AnyMonthOfYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize()
}
constructor(month: Int) : this() { constructor(month: Int) : this() {
listOfValues = arrayListOf(month) listOfValues = arrayListOf(month)
} }
}
class AnyYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return "$value" return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize()
} }
}
class AnyYear() : ListOfInt() {
constructor(year: Int) : this() { constructor(year: Int) : this() {
listOfValues = arrayListOf(year) listOfValues = arrayListOf(year)
} }
override fun labelForValue(value: Int, context: Context): String {
return "$value"
}
} }
object IsWeekDay : TrueQueryCondition() object IsWeekDay : TrueQueryCondition()
@ -564,24 +610,19 @@ sealed class QueryCondition : FilterElementRow {
} }
} }
class PastDay : SingleInt() { class PastDay(value: Int) : SingleInt(value) {
override var operator = Operator.EQUALS override var operator = Operator.EQUALS
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
override fun labelForValue(value: Int, context: Context): String { override fun getDisplayNameWithValues(context: Context): String {
return value.toString() return context.getString(R.string.period_in_days_with_value, this.singleValue.toString())
}
override fun entityName(context: Context): String {
return this.resId?.let {
" " + context.getString(it)
} ?: ""
} }
} }
class Duration : SingleInt() { class Duration(value: Int) : SingleInt(value) {
override var operator = Operator.EQUALS override var operator = Operator.EQUALS
var minutes: Int?
var minutes: Int
get() { get() {
return singleValue return singleValue
} }
@ -589,12 +630,9 @@ sealed class QueryCondition : FilterElementRow {
singleValue = value singleValue = value
} }
val netDuration: Long? val netDuration: Long
get() { get() {
minutes?.let { return (singleValue * 60 * 1000).toLong()
return (it * 60 * 1000).toLong()
}
return null
} }
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
@ -607,24 +645,14 @@ sealed class QueryCondition : FilterElementRow {
object DateNotNull : NotNullQueryCondition() object DateNotNull : NotNullQueryCondition()
object EndDateNotNull : NotNullQueryCondition() object EndDateNotNull : NotNullQueryCondition()
object BigBlindNotNull : NotNullQueryCondition() object BiggestBetNotNull : NotNullQueryCondition()
class StartedFromTime() : TimeQuery() {
override var operator = Operator.MORE
constructor(date: Date) : this() {
singleValue = date
}
class StartedFromTime(date: Date) : TimeQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
} }
class EndedToTime() : TimeQuery() { class EndedToTime(date: Date) : TimeQuery(date) {
override var operator = Operator.LESS override var operator = Operator.LESS_OR_EQUAL
constructor(date: Date) : this() {
singleValue = date
}
} }
interface CustomFieldRelated { interface CustomFieldRelated {
@ -639,12 +667,8 @@ sealed class QueryCondition : FilterElementRow {
} }
} }
class CustomFieldQuery() : QueryDataCondition<CustomField>() { class CustomFieldQuery : QueryDataCondition<CustomField>() {
override var entity: Class<CustomField> = CustomField::class.java override var entity: Class<CustomField> = CustomField::class.java
constructor(customField: CustomField) : this() {
this.setObject(customField)
}
} }
open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated { open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated {
@ -666,7 +690,9 @@ sealed class QueryCondition : FilterElementRow {
val completeLabel = when (listOfValues.size) { val completeLabel = when (listOfValues.size) {
0 -> return NULL_TEXT 0 -> return NULL_TEXT
1, 2 -> { 1, 2 -> {
return name + prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ") return name + prefix + listOfValues.joinToString(", ") {
labelForValue(it, context)
}
} }
else -> "${listOfValues.size} $prefix $name" else -> "${listOfValues.size} $prefix $name"
} }
@ -722,9 +748,16 @@ sealed class QueryCondition : FilterElementRow {
): RealmQuery<T> { ): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java) val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
if (BuildConfig.DEBUG) { // if (BuildConfig.DEBUG) {
fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown // val className = T::class.java
// fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown("fieldName missing for $this, class = $className")
// }
if (fieldName == null) {
val className = T::class.java
Timber.w("Possible missing filter configuration for $this in class $className")
} }
fieldName ?: return realmQuery fieldName ?: return realmQuery
when (this) { when (this) {
@ -762,15 +795,12 @@ sealed class QueryCondition : FilterElementRow {
.lessThanOrEqualTo(fieldName, calendar.time.endOfDay()) .lessThanOrEqualTo(fieldName, calendar.time.endOfDay())
} }
is PastDay -> { is PastDay -> {
singleValue?.let { val startDate = Date()
val startDate = Date() val calendar = Calendar.getInstance()
val calendar = Calendar.getInstance() calendar.time = startDate
calendar.time = startDate calendar.add(Calendar.DAY_OF_YEAR, -singleValue)
calendar.add(Calendar.DAY_OF_YEAR, -it) return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() .lessThanOrEqualTo(fieldName, startDate.endOfDay())
.lessThanOrEqualTo(fieldName, startDate.endOfDay())
}
return realmQuery
} }
is DuringThisWeek -> { is DuringThisWeek -> {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
@ -800,32 +830,25 @@ sealed class QueryCondition : FilterElementRow {
} }
is StartedFromTime -> { is StartedFromTime -> {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
singleValue?.let { calendar.time = singleValue
calendar.time = it realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute()) if (otherQueryCondition is EndedToTime) {
if (otherQueryCondition is EndedToTime) { calendar.time = otherQueryCondition.singleValue
otherQueryCondition.singleValue?.let { endTime -> realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
calendar.time = endTime
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
}
}
} }
return realmQuery return realmQuery
} }
is EndedToTime -> { is EndedToTime -> {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
singleValue?.let { date -> calendar.time = singleValue
calendar.time = date realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute()) if (otherQueryCondition is StartedFromTime) {
if (otherQueryCondition is StartedFromTime) { calendar.time = otherQueryCondition.singleValue
otherQueryCondition.singleValue?.let { startTime -> realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
calendar.time = startTime
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
}
}
} }
return realmQuery return realmQuery
} }
else -> {}
} }
if (this is CustomFieldRelated) { if (this is CustomFieldRelated) {
@ -846,58 +869,62 @@ sealed class QueryCondition : FilterElementRow {
return when (operator) { return when (operator) {
Operator.EQUALS -> { Operator.EQUALS -> {
when (this) { when (this) {
is SingleDate -> realmQuery.equalTo( is SingleDate -> realmQuery.equalTo(fieldName, singleValue)
fieldName, is SingleInt -> realmQuery.equalTo(fieldName, singleValue)
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleInt -> realmQuery.equalTo(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign) is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign)
is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first()) is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first())
else -> realmQuery else -> realmQuery
} }
} }
Operator.MORE -> { Operator.MORE_OR_EQUAL -> {
when (this) { when (this) {
is SingleDate -> realmQuery.greaterThanOrEqualTo( is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue.startOfDay())
fieldName,
singleValue?.startOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.greaterThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is SingleInt -> realmQuery.greaterThan( is Duration -> realmQuery.greaterThanOrEqualTo(fieldName, netDuration)
fieldName, is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing is ListOfInt -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
) is NetAmountLost -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * -1)
is ListOfDouble -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.MORE -> {
when (this) {
is Duration -> realmQuery.greaterThan(fieldName, netDuration)
is SingleInt -> realmQuery.greaterThan(fieldName, singleValue)
is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first())
is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1) is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1)
is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign) is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign)
else -> realmQuery else -> realmQuery
} }
} }
Operator.LESS -> { Operator.LESS_OR_EQUAL -> {
when (this) { when (this) {
is SingleDate -> realmQuery.lessThanOrEqualTo( is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue.endOfDay())
fieldName,
singleValue?.endOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.lessThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is SingleInt -> realmQuery.lessThan( is Duration -> realmQuery.lessThanOrEqualTo(fieldName, netDuration)
fieldName, is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing is ListOfInt -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
) is NetAmountLost -> {
realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * -1)
realmQuery.lessThan(fieldName, 0.0)
}
is NetAmountWon -> {
realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
realmQuery.greaterThan(fieldName, 0.0)
}
is ListOfDouble -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.LESS -> {
when (this) {
is Duration -> realmQuery.lessThan(fieldName, netDuration)
is SingleInt -> realmQuery.lessThan(fieldName, singleValue)
is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first())
is NetAmountLost -> { is NetAmountLost -> {
realmQuery.greaterThan(fieldName, listOfValues.first() * -1) realmQuery.greaterThan(fieldName, listOfValues.first() * -1)
@ -953,8 +980,8 @@ sealed class QueryCondition : FilterElementRow {
is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal Operator.MORE, Operator.MORE_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal Operator.LESS, Operator.LESS_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> RowViewType.TITLE_CHECK.ordinal else -> RowViewType.TITLE_CHECK.ordinal
} }
} }
@ -967,8 +994,8 @@ sealed class QueryCondition : FilterElementRow {
is PastDay -> BottomSheetType.EDIT_TEXT is PastDay -> BottomSheetType.EDIT_TEXT
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE -> BottomSheetType.EDIT_TEXT Operator.MORE, Operator.MORE_OR_EQUAL -> BottomSheetType.EDIT_TEXT
Operator.LESS -> BottomSheetType.EDIT_TEXT Operator.LESS, Operator.LESS_OR_EQUAL -> BottomSheetType.EDIT_TEXT
else -> BottomSheetType.NONE else -> BottomSheetType.NONE
} }
} }
@ -995,27 +1022,27 @@ sealed class QueryCondition : FilterElementRow {
is IsWeekDay -> R.string.week_days is IsWeekDay -> R.string.week_days
is IsWeekEnd -> R.string.weekend is IsWeekEnd -> R.string.weekend
is PastDay -> R.string.period_in_days is PastDay -> R.string.period_in_days
is TournamentNumberOfPlayer -> { // is TournamentNumberOfPlayer -> {
when (this.operator) { // when (this.operator) {
Operator.MORE -> R.string.minimum // Operator.MORE -> R.string.minimum
Operator.LESS -> R.string.maximum // Operator.LESS -> R.string.maximum
else -> null // else -> null
} // }
} // }
is NetAmountWon -> { // is NetAmountWon -> {
when (this.operator) { // when (this.operator) {
Operator.MORE -> R.string.won_amount_more_than // Operator.MORE -> R.string.won_amount_more_than
Operator.LESS -> R.string.won_amount_less_than // Operator.LESS -> R.string.won_amount_less_than
else -> null // else -> null
} // }
} // }
is NetAmountLost -> { // is NetAmountLost -> {
when (this.operator) { // when (this.operator) {
Operator.MORE -> R.string.lost_amount_more_than // Operator.MORE -> R.string.lost_amount_more_than
Operator.LESS -> R.string.lost_amount_less_than // Operator.LESS -> R.string.lost_amount_less_than
else -> null // else -> null
} // }
} // }
is TournamentFinalPosition -> { is TournamentFinalPosition -> {
when (this.operator) { when (this.operator) {
Operator.MORE -> R.string.minimum Operator.MORE -> R.string.minimum
@ -1025,8 +1052,10 @@ sealed class QueryCondition : FilterElementRow {
} }
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE -> R.string.more_than Operator.MORE_OR_EQUAL -> R.string.more_or_equal_sign
Operator.LESS -> R.string.less_than Operator.MORE -> R.string.more_sign
Operator.LESS_OR_EQUAL -> R.string.less_or_equal_sign
Operator.LESS -> R.string.less_sign
else -> null else -> null
} }
} }

@ -1,80 +0,0 @@
package net.pokeranalytics.android.model.handhistory
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.handhistory.Card
/***
* An interface used for board changes notifications
*/
interface BoardChangedListener {
fun boardChanged()
}
/***
* The BoardManager purpose is to manage the cards from a hand history board
* and notify its listener when a change occurs
*/
class BoardManager(cards: List<Card>, var listener: BoardChangedListener) {
/***
* The sorted list of cards
*/
private var sortedBoardCards: MutableList<Card> = mutableListOf()
/***
* All cards
*/
val allCards: List<Card>
get() {
return this.sortedBoardCards
}
init {
this.sortedBoardCards = cards.sortedBy { it.index }.toMutableList()
}
/***
* Adds a card to the board, notifies the listener
*/
fun add(card: Card) {
this.sortedBoardCards.lastOrNull()?.let {
if (it.suit == null) {
it.suit = Card.Suit.UNDEFINED
}
}
if (this.sortedBoardCards.size == 5) {
throw PAIllegalStateException("Can't add anymore cards")
}
card.index = this.sortedBoardCards.size
this.sortedBoardCards.add(card)
this.listener.boardChanged()
}
/***
* Clears the street's cards, notifies the listener
*/
fun clearStreet(street: Street) {
this.sortedBoardCards.removeAll { it.street == street }
this.listener.boardChanged()
}
/***
* Returns the last card of a given [street]
*/
fun lastCard(street: Street) : Card? {
return this.sortedBoardCards.lastOrNull { it.street == street }
}
/***
* Remove the given [card], notifies the listener
*/
fun remove(card: Card) {
this.sortedBoardCards.remove(card)
this.listener.boardChanged()
}
}

@ -6,7 +6,6 @@ import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber import timber.log.Timber
import java.util.*
class HandSetup { class HandSetup {
@ -36,9 +35,31 @@ class HandSetup {
} }
var type: Session.Type? = null
var blinds: String? = null
// var bigBlind: Double? = null
var ante: Double? = null
var tableSize: Int? = null
var bigBlindAnte: Boolean = false
var game: Game? = null
var session: Session? = null
var straddlePositions: MutableList<Position> = mutableListOf()
private set
fun clearStraddles() {
this.straddlePositions.clear()
}
private fun configure(handHistory: HandHistory) { private fun configure(handHistory: HandHistory) {
this.smallBlind = handHistory.smallBlind this.blinds = handHistory.blinds
this.bigBlind = handHistory.bigBlind
this.bigBlindAnte = handHistory.bigBlindAnte this.bigBlindAnte = handHistory.bigBlindAnte
this.ante = handHistory.ante this.ante = handHistory.ante
this.tableSize = handHistory.numberOfPlayers this.tableSize = handHistory.numberOfPlayers
@ -56,32 +77,15 @@ class HandSetup {
this.game = session.game // we don't want to force the max number of cards if unsure this.game = session.game // we don't want to force the max number of cards if unsure
} }
this.type = session.sessionType this.type = session.sessionType
this.smallBlind = session.cgSmallBlind this.blinds = session.cgBlinds
this.bigBlind = session.cgBigBlind this.ante = session.cgAnte
this.tableSize = session.tableSize this.tableSize = session.tableSize
}
var type: Session.Type? = null
var smallBlind: Double? = null
var bigBlind: Double? = null
var ante: Double? = null
var tableSize: Int? = null
var bigBlindAnte: Boolean = false
var game: Game? = null
var session: Session? = null
var straddlePositions: MutableList<Position> = mutableListOf() val blindValues = session.blindValues
private set if (blindValues.size > 2) {
this.straddlePositions = Position.positionsPerPlayers(10).drop(2).take(blindValues.size - 2).toMutableList()
}
fun clearStraddles() {
this.straddlePositions.clear()
} }
/*** /***

@ -5,7 +5,6 @@ import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.ui.view.RowRepresentable
enum class SaveValidityStatus { enum class SaveValidityStatus {
VALID, VALID,
@ -23,7 +22,7 @@ enum class DeleteValidityStatus {
/** /**
* An interface to group object which are managed by the database * An interface to group object which are managed by the database
*/ */
interface Manageable : Savable, Deletable, Editable interface Manageable : Savable, Deletable
interface NameManageable : Manageable { interface NameManageable : Manageable {
var name: String var name: String
@ -68,17 +67,6 @@ interface Identifiable : RealmModel {
} }
} }
/**
* An interface to update the fields of an object
*/
interface Editable : Identifiable {
/**
* a method to handle the modification of the object.
* Through [RowRepresentable] the object is able to update the right variable with the new value.
*/
fun updateValue(value: Any?, row: RowRepresentable)
}
/** /**
* An interface to easily handle the validity of any object we want to save * An interface to easily handle the validity of any object we want to save
*/ */

@ -0,0 +1,189 @@
package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.toCurrency
import java.lang.Integer.min
import java.text.NumberFormat
import java.text.ParseException
import java.util.*
data class CodedStake(var stakes: String) : Comparable<CodedStake> {
var ante: Double? = null
var blinds: String? = null
var currency: Currency
init {
var currencyCode: String? = null
val parameters = this.stakes.split(StakesHolder.cbSeparator)
parameters.forEach { param ->
when {
param.contains(StakesHolder.cbAnte) -> ante = param.removePrefix(StakesHolder.cbAnte).let { NumberFormat.getInstance().parse(it)?.toDouble() }
param.contains(StakesHolder.cbBlinds) -> blinds = param.removePrefix(StakesHolder.cbBlinds)
param.contains(StakesHolder.cbCode) -> currencyCode = param.removePrefix(
StakesHolder.cbCode
)
}
}
this.currency = currencyCode?.let { Currency.getInstance(it) }
?: run { UserDefaults.currency }
}
override fun compareTo(other: CodedStake): Int {
if (this.currency == other.currency) {
this.blinds?.let { b1 ->
other.blinds?.let { b2 ->
if (b1 == b2) {
return this.compareAnte(other)
} else {
val bv1 = this.reversedBlindsArray(b1)
val bv2 = this.reversedBlindsArray(b2)
for (i in 0 until min(bv1.size, bv2.size)) {
if (bv1[i] != bv2[i]) {
return bv1[i].compareTo(bv2[i])
} else {
continue
}
}
return bv1.size.compareTo(bv2.size)
}
} ?: run {
return 1
}
} ?: run {
return this.compareAnte(other)
}
} else {
return this.currency.currencyCode.compareTo(other.currency.currencyCode)
}
}
private fun compareAnte(other: CodedStake): Int {
this.ante?.let { a1 ->
other.ante?.let { a2 ->
return a1.compareTo(a2)
} ?: run {
return 1
}
} ?: run {
return -1
}
}
private fun reversedBlindsArray(blinds: String): List<Double> {
return blinds.split(BLIND_SEPARATOR).mapNotNull { NumberFormat.getInstance().parse(it)?.toDouble() }.reversed()
}
fun formattedStakes(): String {
val components = arrayListOf<String>()
this.formattedBlinds()?.let { components.add(it) }
if ((this.ante ?: -1.0) > 0.0) {
this.formattedAnte()?.let { components.add("($it)") }
}
return if (components.isNotEmpty()) {
components.joinToString(" ")
} else {
NULL_TEXT
}
}
private fun formattedBlinds(): String? {
this.blinds?.let {
val placeholder = 1.0
val regex = Regex("-?\\d+(\\.\\d+)?")
return placeholder.toCurrency(currency).replace(regex, it)
}
return null
}
private fun formattedAnte(): String? {
this.ante?.let {
return it.toCurrency(this.currency)
}
return null
}
}
interface StakesHolder {
companion object {
const val cbSeparator = ";"
const val cbAnte = "A="
const val cbBlinds = "B="
const val cbCode = "C="
fun readableStakes(value: String): String {
return CodedStake(value).formattedStakes()
}
}
val ante: Double?
val blinds: String?
val biggestBet: Double?
val stakes: String?
val bankroll: Bankroll?
fun setHolderStakes(stakes: String?)
fun setHolderBiggestBet(biggestBet: Double?)
val blindValues: List<Double>
get() {
this.blinds?.let { blinds ->
val blindsSplit = blinds.split(BLIND_SEPARATOR)
return blindsSplit.mapNotNull {
try {
NumberFormat.getInstance().parse(it)?.toDouble()
} catch (e: ParseException) {
null
}
}
}
return listOf()
}
fun generateStakes() {
if (this.ante == null && this.blinds == null) {
setHolderStakes(null)
return
}
val components = arrayListOf<String>()
this.blinds?.let { components.add("${cbBlinds}${it}") }
this.ante?.let { components.add("${cbAnte}${it.formatted}") }
val code = this.bankroll?.currency?.code ?: UserDefaults.currency.currencyCode
components.add("${cbCode}${code}")
setHolderStakes(components.joinToString(cbSeparator))
}
fun defineHighestBet() {
val bets = arrayListOf<Double>()
this.ante?.let { bets.add(it) }
bets.addAll(this.blindValues)
setHolderBiggestBet(bets.maxOrNull())
}
}

@ -5,7 +5,7 @@ import io.realm.RealmModel
/** /**
* An interface to be able to track the usage of an object * An interface to be able to track the usage of an object
*/ */
interface CountableUsage : Identifiable { interface UsageCountable : Identifiable {
var useCount: Int var useCount: Int
get() { return 0 } get() { return 0 }
set(_) {} set(_) {}

@ -2,22 +2,34 @@ package net.pokeranalytics.android.model.migrations
import android.content.Context import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.where
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.SessionSetManager import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import java.text.NumberFormat
class Patcher { class Patcher {
companion object { companion object {
fun patchAll(context: Context) { fun patchAll(application: PokerAnalyticsApplication) {
patchMissingTransactionTypes(context)
val context = application.applicationContext
patchSessionSet() // NOTE: it's more than possible that at one point many patches become redundant
// with each other
patchMissingTransactionTypes(context)
Preferences.executeOnce(Preferences.Keys.PATCH_COMPUTABLE_RESULTS, context) {
patchComputableResults()
}
Preferences.executeOnce(Preferences.Keys.PATCH_SESSION_SETS, context) { Preferences.executeOnce(Preferences.Keys.PATCH_SESSION_SETS, context) {
patchSessionSet() patchSessionSet()
} }
@ -27,51 +39,42 @@ class Patcher {
Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) { Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) {
patchDefaultTransactionTypes(context) patchDefaultTransactionTypes(context)
} }
Preferences.executeOnce(Preferences.Keys.PATCH_BLINDS_FORMAT, context) { Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) {
patchBlindFormat() patchStakes()
}
Preferences.executeOnce(Preferences.Keys.PATCH_NEGATIVE_LIMITS, context) {
patchNegativeLimits()
}
Preferences.executeOnce(Preferences.Keys.CLEAN_BLINDS_FILTERS, context) {
cleanBlindsFilters()
} }
val realm = Realm.getDefaultInstance() Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
patchMissingTransactionTypes(context)
}
val lockedTypes = Preferences.executeOnce(Preferences.Keys.PATCH_ZERO_TABLE, context) {
realm.where(TransactionType::class.java).equalTo("lock", true).findAll() patchZeroTable()
if (lockedTypes.size == 3) {
Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
val newTypes = arrayOf(
TransactionType.Value.STACKING_INCOMING,
TransactionType.Value.STACKING_OUTGOING
)
realm.executeTransaction {
Seed.createDefaultTransactionTypes(newTypes, context, realm)
}
}
} }
realm.close() Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) {
patchRatedAmounts()
}
patchPerformances(application)
} }
private fun patchMissingTransactionTypes(context: Context) { private fun patchMissingTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val depositType = TransactionType.Value.DEPOSIT val transactionTypes = TransactionType.Value.values()
val deposit = realm.where(TransactionType::class.java)
.equalTo("kind", depositType.uniqueIdentifier).findFirst()
if (deposit == null) {
realm.executeTransaction {
Seed.createDefaultTransactionTypes(arrayOf(depositType), context, realm)
}
}
val withdrawalType = TransactionType.Value.WITHDRAWAL realm.executeTransaction {
val withdrawal = realm.where(TransactionType::class.java) Seed.createDefaultTransactionTypes(transactionTypes, context, realm)
.equalTo("kind", withdrawalType.uniqueIdentifier).findFirst()
if (withdrawal == null) {
realm.executeTransaction {
Seed.createDefaultTransactionTypes(arrayOf(withdrawalType), context, realm)
}
} }
realm.close()
realm.close()
} }
private fun patchBreaks() { private fun patchBreaks() {
@ -86,7 +89,8 @@ class Patcher {
it.computeStats() it.computeStats()
} }
sessions.forEach { sessions.forEach {
it.formatBlinds() it.generateStakes()
it.defineHighestBet()
} }
results.forEach { results.forEach {
it.computeNumberOfRebuy() it.computeNumberOfRebuy()
@ -111,13 +115,50 @@ class Patcher {
realm.close() realm.close()
} }
private fun patchBlindFormat() { private fun patchStakes() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransaction {
val sessions = realm.where(Session::class.java).findAll() val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session -> sessions.forEach { session ->
session.formatBlinds() val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
session.cgAnte = null
if (blindsFormatted.isNotEmpty()) {
session.cgBlinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
} }
val handHistories = realm.where(HandHistory::class.java).findAll()
handHistories.forEach { hh ->
val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
if (blindsFormatted.isNotEmpty()) {
hh.blinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
}
}
realm.close()
}
private fun patchNegativeLimits() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).lessThan("limit", 0).findAll()
sessions.forEach { session ->
session.limit = null
}
}
realm.close()
}
private fun cleanBlindsFilters() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val blindFilterConditions = realm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll()
val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id }
val filters = realm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll()
filters.deleteAllFromRealm()
} }
realm.close() realm.close()
} }
@ -139,6 +180,55 @@ class Patcher {
realm.close() realm.close()
} }
} /*
15/04/21: Two needs:
- To get better performance for ITM Ratio, a positive session is a net >= 0 for cash game, or a cashedOut > 0 for tournaments
- The field "ratedTips" is added
*/
private fun patchComputableResults() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val crs = realm.where(ComputableResult::class.java).findAll()
crs.forEach { cr ->
cr.session?.let { cr.updateWith(it) }
}
}
realm.close()
}
private fun patchPerformances(application: PokerAnalyticsApplication) {
val realm = Realm.getDefaultInstance()
val sessionCount = realm.where<Session>().findAll().size
val performanceCount = realm.where<Performance>().findAll().size
if (sessionCount > 1 && performanceCount == 0) {
application.reportWhistleBlower?.requestReportLaunch()
}
realm.close()
}
private fun patchZeroTable() {
val realm = Realm.getDefaultInstance()
val zero = 0
val sessions = realm.where<Session>().equalTo("numberOfTables", zero).findAll()
realm.executeTransaction {
sessions.forEach { s ->
s.numberOfTables = 1
}
}
realm.close()
}
private fun patchRatedAmounts() {
val realm = Realm.getDefaultInstance()
val transactions = realm.where<Transaction>().findAll()
realm.executeTransaction {
transactions.forEach { t ->
t.computeRatedAmount()
}
}
realm.close()
}
}
} }

@ -9,7 +9,7 @@ import java.util.*
class PokerAnalyticsMigration : RealmMigration { class PokerAnalyticsMigration : RealmMigration {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
// DynamicRealm exposes an editable schema // DynamicRealm exposes an editable schema
val schema = realm.schema val schema = realm.schema
@ -21,7 +21,8 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 0) { if (currentVersion == 0) {
Timber.d("*** Running migration 1") Timber.d("*** Running migration 1")
schema.get("Filter")?.addField("entityType", Int::class.java)?.setNullable("entityType", true) schema.get("Filter")?.addField("entityType", Int::class.java)
?.setNullable("entityType", true)
schema.get("FilterElement")?.let { schema.get("FilterElement")?.let {
it.setNullable("filterName", true) it.setNullable("filterName", true)
it.setNullable("sectionName", true) it.setNullable("sectionName", true)
@ -83,7 +84,8 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 3) { if (currentVersion == 3) {
Timber.d("*** Running migration ${currentVersion + 1}") Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Result")?.addField("numberOfRebuy", Double::class.java)?.setNullable("numberOfRebuy", true) schema.get("Result")?.addField("numberOfRebuy", Double::class.java)
?.setNullable("numberOfRebuy", true)
currentVersion++ currentVersion++
} }
@ -117,7 +119,8 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("CustomField")?.let { schema.get("CustomField")?.let {
it.addField("type", Integer::class.java).setNullable("type", false) it.addField("type", Integer::class.java).setNullable("type", false)
it.addField("duplicateValue", Boolean::class.java) it.addField("duplicateValue", Boolean::class.java)
it.addField("sortCondition", Integer::class.java).setRequired("sortCondition", true) it.addField("sortCondition", Integer::class.java)
.setRequired("sortCondition", true)
it.addRealmListField("entries", customFieldEntrySchema) it.addRealmListField("entries", customFieldEntrySchema)
} }
@ -134,7 +137,8 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("ReportSetup")?.let { schema.get("ReportSetup")?.let {
it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true) it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true)
it.addRealmListField("criteriaCustomFieldIds", String::class.java) it.addRealmListField("criteriaCustomFieldIds", String::class.java)
it.addRealmListField("criteriaIds", Int::class.java).setNullable("criteriaIds", true) it.addRealmListField("criteriaIds", Int::class.java)
.setNullable("criteriaIds", true)
it.removeField("filters") it.removeField("filters")
schema.get("Filter")?.let { filterSchema -> schema.get("Filter")?.let { filterSchema ->
it.addRealmObjectField("filter", filterSchema) it.addRealmObjectField("filter", filterSchema)
@ -195,7 +199,8 @@ class PokerAnalyticsMigration : RealmMigration {
val cardSchema = schema.create("Card") val cardSchema = schema.create("Card")
cardSchema.addField("value", Int::class.java).setRequired("value", false) cardSchema.addField("value", Int::class.java).setRequired("value", false)
cardSchema.addField("suitIdentifier", Int::class.java).setRequired("suitIdentifier", false) cardSchema.addField("suitIdentifier", Int::class.java)
.setRequired("suitIdentifier", false)
cardSchema.addField("index", Int::class.java) cardSchema.addField("index", Int::class.java)
hhSchema.addRealmListField("board", cardSchema) hhSchema.addRealmListField("board", cardSchema)
@ -203,10 +208,12 @@ class PokerAnalyticsMigration : RealmMigration {
actionSchema.addField("streetIdentifier", Int::class.java) actionSchema.addField("streetIdentifier", Int::class.java)
actionSchema.addField("index", Int::class.java) actionSchema.addField("index", Int::class.java)
actionSchema.addField("position", Int::class.java) actionSchema.addField("position", Int::class.java)
actionSchema.addField("typeIdentifier", Int::class.java).setRequired("typeIdentifier", false) actionSchema.addField("typeIdentifier", Int::class.java)
.setRequired("typeIdentifier", false)
actionSchema.addField("amount", Double::class.java).setRequired("amount", false) actionSchema.addField("amount", Double::class.java).setRequired("amount", false)
actionSchema.addField("effectiveAmount", Double::class.java) actionSchema.addField("effectiveAmount", Double::class.java)
actionSchema.addField("positionRemainingStack", Double::class.java).setRequired("positionRemainingStack", false) actionSchema.addField("positionRemainingStack", Double::class.java)
.setRequired("positionRemainingStack", false)
hhSchema.addRealmListField("actions", actionSchema) hhSchema.addRealmListField("actions", actionSchema)
val playerSetupSchema = schema.create("PlayerSetup") val playerSetupSchema = schema.create("PlayerSetup")
@ -227,13 +234,115 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++ currentVersion++
} }
// Migrate to version 10
if (currentVersion == 9) {
schema.get("Session")?.addField("handsCount", Int::class.java)
?.setRequired("handsCount", false)
schema.get("Session")?.transform { obj -> // otherwise we get a 0 default value
obj.setNull("handsCount")
}
val configSchema = schema.create("UserConfig")
configSchema.addField("id", String::class.java).setRequired("id", true)
configSchema.addPrimaryKey("id")
configSchema.addField("liveDealtHandsPerHour", Int::class.java)
configSchema.addField("onlineDealtHandsPerHour", Int::class.java)
currentVersion++
}
// Migrate to version 11
if (currentVersion == 10) {
schema.get("ComputableResult")?.addField("ratedTips", Double::class.java)
currentVersion++
}
// Migrate to version 12
if (currentVersion == 11) {
schema.get("Session")?.let { ss ->
ss.addField("cgAnte", Double::class.java)
?.setNullable("cgAnte", true)
ss.addField("cgBiggestBet", Double::class.java)
?.setNullable("cgBiggestBet", true)
ss.addField("cgStakes", String::class.java)
ss.addField("cgBlinds", String::class.java)
ss.removeField("blinds")
ss.renameField("cgSmallBlind", "cgOldSmallBlind")
ss.renameField("cgBigBlind", "cgOldBigBlind")
}
schema.get("HandHistory")?.let { hs ->
hs.setNullable("ante", true)
hs.addField("stakes", String::class.java)
hs.addField("blinds", String::class.java)
hs.addField("biggestBet", Double::class.java)
?.setNullable("biggestBet", true)
hs.renameField("smallBlind", "oldSmallBlind")
hs.renameField("bigBlind", "oldBigBlind")
}
currentVersion++
}
// Migrate to version 13
if (currentVersion == 12) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("transferRate", Double::class.java)
.setNullable("transferRate", true)
schema.get("Bankroll")?.let { bs ->
ts.addRealmObjectField("destination", bs)
} ?: throw PAIllegalStateException("Bankroll schema not found")
}
schema.create("Performance")?.let { ps ->
ps.addField("id", String::class.java).setRequired("id", true)
ps.addPrimaryKey("id")
ps.addField("reportId", Int::class.java)
ps.addField("key", Int::class.java)
ps.addField("name", String::class.java)
ps.addField("objectId", String::class.java)//.setNullable("objectId", true)
ps.addField("customFieldId", String::class.java)//.setNullable("customFieldId", true)
ps.addField("value", Double::class.java).setRequired("value", false) //.setNullable("value", true)
}
currentVersion++
}
// Migrate to version 14
if (currentVersion == 13) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("ratedAmount", Double::class.java)
} ?: throw PAIllegalStateException("Transaction schema not found")
//transactionTypeIds
schema.get("UserConfig")?.let { ucs ->
ucs.addField("transactionTypeIds", String::class.java).setRequired("transactionTypeIds", true)
} ?: throw PAIllegalStateException("UserConfig schema not found")
schema.get("Performance")?.let { ps ->
if (!ps.isPrimaryKey("id")) {
ps.addPrimaryKey("id")
}
}
currentVersion++
}
}
override fun equals(other: Any?): Boolean {
return other is RealmMigration
} }
override fun equals(other: Any?): Boolean { override fun hashCode(): Int {
return other is RealmMigration return RealmMigration::javaClass.hashCode()
} }
override fun hashCode(): Int {
return RealmMigration::javaClass.hashCode()
}
} }

@ -14,12 +14,37 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.BankrollRow import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.rows.BankrollPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import java.util.* import java.util.*
open class Bankroll : RealmObject(), NameManageable, RowRepresentable { enum class ResultCaptureType {
BUYIN_CASHED_OUT,
NET_RESULT;
companion object {
val buyinCashedOutFields = listOf(
SessionPropertiesRow.CASHED_OUT,
SessionPropertiesRow.BUY_IN,
SessionPropertiesRow.TIPS)
val netResultFields = listOf(SessionPropertiesRow.NET_RESULT)
}
val rowRepresentables: List<RowRepresentable>
get() {
return when (this) {
BUYIN_CASHED_OUT -> buyinCashedOutFields
NET_RESULT -> netResultFields
}
}
}
open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresentable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -35,6 +60,12 @@ open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
@LinkingObjects("bankroll") @LinkingObjects("bankroll")
val transactions: RealmResults<Transaction>? = null val transactions: RealmResults<Transaction>? = null
/**
* The list of transactions where the bankroll is the destination
*/
@LinkingObjects("destination")
val destinationTransactions: RealmResults<Transaction>? = null
// The currency of the bankroll // The currency of the bankroll
var currency: Currency? = null var currency: Currency? = null
@ -50,25 +81,6 @@ open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
return this.name return this.name
} }
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollRow.LIVE -> {
this.live = if (value is Boolean) !value else false
}
BankrollRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
override fun isValidForDelete(realm: Realm): Boolean { override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty() return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty()
&& realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty() && realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()
@ -100,6 +112,16 @@ open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
} }
} }
fun resultCaptureType(context: Context): ResultCaptureType {
return Preferences.getResultCaptureType(this, context)
?: run {
when (this.live) {
true -> ResultCaptureType.BUYIN_CASHED_OUT
else -> ResultCaptureType.NET_RESULT
}
}
}
companion object { companion object {
fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll { fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll {
@ -121,6 +143,25 @@ open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
} }
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollPropertiesRow.ONLINE -> {
this.live = if (value is Boolean) !value else false
}
BankrollPropertiesRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollPropertiesRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollPropertiesRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
val utilCurrency: java.util.Currency val utilCurrency: java.util.Currency
get() { get() {
this.currency?.code?.let { this.currency?.code?.let {

@ -13,11 +13,12 @@ import net.pokeranalytics.android.model.interfaces.Manageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
open class Comment : RealmObject(), Manageable, RowRepresentable { open class Comment : RealmObject(), Manageable, RowRepresentable, RowUpdatable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()

@ -4,7 +4,7 @@ import io.realm.RealmObject
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
open class ComputableResult() : RealmObject(), Filterable { open class ComputableResult : RealmObject(), Filterable {
var ratedNet: Double = 0.0 var ratedNet: Double = 0.0
@ -22,6 +22,8 @@ open class ComputableResult() : RealmObject(), Filterable {
var session: Session? = null var session: Session? = null
var ratedTips: Double = 0.0
fun updateWith(session: Session) { fun updateWith(session: Session) {
val rate = session.bankroll?.currency?.rate ?: 1.0 val rate = session.bankroll?.currency?.rate ?: 1.0
@ -30,10 +32,11 @@ open class ComputableResult() : RealmObject(), Filterable {
this.ratedNet = result.net * rate this.ratedNet = result.net * rate
this.isPositive = result.isPositive this.isPositive = result.isPositive
this.ratedBuyin = (result.buyin ?: 0.0) * rate this.ratedBuyin = (result.buyin ?: 0.0) * rate
this.ratedTips = (result.tips ?: 0.0) * rate
} }
this.bbNet = session.bbNet this.bbNet = session.bbNet
this.hasBigBlind = if (session.cgBigBlind != null) 1 else 0 this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0
this.estimatedHands = session.estimatedHands this.estimatedHands = session.estimatedHands
this.bbPer100Hands = this.bbPer100Hands =
session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
@ -47,6 +50,7 @@ open class ComputableResult() : RealmObject(), Filterable {
IS_POSITIVE("isPositive"), IS_POSITIVE("isPositive"),
RATED_BUYIN("ratedBuyin"), RATED_BUYIN("ratedBuyin"),
ESTIMATED_HANDS("estimatedHands"), ESTIMATED_HANDS("estimatedHands"),
RATED_TIPS("ratedTips"),
// BB_PER100HANDS("bbPer100Hands") // BB_PER100HANDS("bbPer100Hands")
} }

@ -18,17 +18,18 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
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.rowrepresentable.CustomFieldRow import net.pokeranalytics.android.ui.view.rows.CustomFieldPropertiesRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable { open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource {
companion object { companion object {
@ -111,13 +112,9 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
@Ignore @Ignore
private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList() private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList()
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
@Ignore @Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf() private var rowRepresentation: List<RowRepresentable> = mutableListOf()
//helper //helper
val isListType: Boolean val isListType: Boolean
@ -130,14 +127,6 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
return this.type == Type.AMOUNT.uniqueIdentifier return this.type == Type.AMOUNT.uniqueIdentifier
} }
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation return rowRepresentation
} }
@ -145,8 +134,8 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SimpleRow.NAME -> this.name = value as String? ?: "" SimpleRow.NAME -> this.name = value as String? ?: ""
CustomFieldRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier CustomFieldPropertiesRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false CustomFieldPropertiesRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
} }
} }
@ -178,13 +167,6 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
return R.string.cf_entry_delete_popup_message return R.string.cf_entry_delete_popup_message
} }
override val bottomSheetType: BottomSheetType
get() {
return when (type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun deleteDependencies(realm: Realm) { override fun deleteDependencies(realm: Realm) {
if (isValid) { if (isValid) {
@ -204,35 +186,13 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
} }
} }
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
return when (type) {
Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
/** /**
* Update the row representation * Update the row representation
*/ */
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> { private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME) rows.add(SimpleRow.NAME)
rows.add(CustomFieldRow.TYPE) rows.add(CustomFieldPropertiesRow.TYPE)
if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) { if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) {
if (entries.isNotEmpty()) { if (entries.isNotEmpty()) {
@ -343,4 +303,45 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
} }
} }
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.type) {
Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
} }

@ -18,6 +18,7 @@ import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
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.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.toCurrency import net.pokeranalytics.android.util.extensions.toCurrency
@ -26,7 +27,7 @@ import java.util.*
import java.util.Currency import java.util.Currency
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable { open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = CustomFieldEntry::class.java override val realmObjectClass: Class<out Identifiable> = CustomFieldEntry::class.java

@ -12,11 +12,9 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.view.ImageDecorator import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.rows.FilterItemRow
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -25,7 +23,7 @@ import java.util.*
* It contains a list of [FilterCondition] describing the complete query to launch * It contains a list of [FilterCondition] describing the complete query to launch
* The [Filter] is working closely with a [Filterable] interface providing the entity we want the query being launched on * The [Filter] is working closely with a [Filterable] interface providing the entity we want the query being launched on
*/ */
open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, CountableUsage, ImageDecorator { open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, UsageCountable, ImageDecorator {
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = Filter::class.java override val realmObjectClass: Class<out Identifiable> = Filter::class.java
@ -89,22 +87,25 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
return FilterableType.ALL return FilterableType.ALL
} }
fun createOrUpdateFilterConditions(filterConditionRows: ArrayList<QueryCondition>) { fun createOrUpdateFilterConditions(filterConditionRows: List<FilterItemRow>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.id }}")
Timber.d("list of querys saving: ${filterConditionRows.map { it.queryCondition?.id }}")
Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}") Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}")
filterConditionRows filterConditionRows
.map { .mapNotNull {
it.groupId it.queryCondition?.groupId
} }
.distinct() .distinct()
.forEach { groupId -> .forEach { groupId ->
filterConditionRows filterConditionRows
.filter { .filter {
it.groupId == groupId it.queryCondition?.groupId == groupId
} }
.apply { .apply {
Timber.d("list of querys: ${this.map { it.id }}") val conditions = this.mapNotNull { it.queryCondition }
val newFilterCondition = FilterCondition(this) Timber.d("list of querys: ${conditions.map { it.id }}")
val newFilterCondition = FilterCondition(conditions, this.first().filterSectionRow)
val previousCondition = filterConditions.filter { val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
} }
@ -132,19 +133,13 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}") Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}")
Timber.d("list of contains ${filterElementRow.id}") Timber.d("list of contains ${filterElementRow.id}")
val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first()) val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first())
Timber.d("list of : $contained") Timber.d("is contained: $contained")
return contained return contained
} }
/** fun filterCondition(filterElementRow: QueryCondition): FilterCondition? {
* Get the saved value for the given [filterElementRow] return filterConditions.firstOrNull {
*/ it.queryCondition.id.contains(filterElementRow.id.first())
fun loadValueForElement(filterElementRow: QueryCondition) {
val filtered = filterConditions.filter {
it.queryCondition.id == filterElementRow.id
}
if (filtered.isNotEmpty()) {
return filterElementRow.updateValueBy(filtered.first())
} }
} }

@ -2,19 +2,21 @@ package net.pokeranalytics.android.model.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.LinkingObjects
import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class FilterCondition() : RealmObject() { open class FilterCondition() : RealmObject() {
private constructor(filterName:String, sectionName:String) : this() { private constructor(filterName: String, sectionName: String) : this() {
this.filterName = filterName this.filterName = filterName
this.sectionName = sectionName this.sectionName = sectionName
} }
constructor(filterElementRows: List<QueryCondition>) : this(filterElementRows.first().baseId, filterElementRows.first().filterSectionRow.name) { constructor(filterElementRows: List<QueryCondition>, section: FilterSectionRow) : this(filterElementRows.first().baseId, section.name) {
val row = filterElementRows.first() val row = filterElementRows.first()
this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName
this.operator = row.operator.ordinal this.operator = row.operator.ordinal
@ -22,11 +24,12 @@ open class FilterCondition() : RealmObject() {
this.stringValue = row.customFieldId this.stringValue = row.customFieldId
} }
when (row) { when (row) {
is QueryCondition.SingleInt -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing) is QueryCondition.SingleInt -> this.setValue(row.singleValue)
is QueryCondition.SingleDate -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing) is QueryCondition.SingleDate -> this.setValue(row.singleValue)
is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues }) is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues })
is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues }) is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues })
is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues }) is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues })
else -> {}
} }
} }
@ -48,7 +51,10 @@ open class FilterCondition() : RealmObject() {
var stringValue: String? = null var stringValue: String? = null
var operator: Int? = null var operator: Int? = null
inline fun <reified T:Any > getValues(): ArrayList < T > { @LinkingObjects("filterConditions")
val filters: RealmResults<Filter>? = null
inline fun <reified T> getValues(): ArrayList <T> {
return when (T::class) { return when (T::class) {
Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } } Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } }
Double::class -> ArrayList<T>().apply { doubleValues?.map { add(it as T) } } Double::class -> ArrayList<T>().apply { doubleValues?.map { add(it as T) } }
@ -57,7 +63,17 @@ open class FilterCondition() : RealmObject() {
} }
} }
inline fun <reified T:Any > getValue(): T { fun <T> getv(clazz: Class<T>) : T {
return when (clazz) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
Date::class -> dateValue ?: Date()
String::class -> stringValue ?: ""
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} as T
}
inline fun <reified T> getValue(): T {
return when (T::class) { return when (T::class) {
Int::class -> intValue ?: 0 Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0 Double::class -> doubleValue?: 0.0
@ -91,4 +107,5 @@ open class FilterCondition() : RealmObject() {
fun setValue(value:String) { fun setValue(value:String) {
stringValue = value stringValue = value
} }
} }

@ -8,20 +8,21 @@ import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.CountableUsage import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
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.rowrepresentable.GameRow import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.rows.GamePropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, CountableUsage { open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable, UsageCountable {
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = Game::class.java override val realmObjectClass: Class<out Identifiable> = Game::class.java
@ -71,7 +72,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
): CharSequence { ): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
GameRow.SHORT_NAME -> this.shortName ?: NULL_TEXT GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0) else -> return super.charSequenceForRow(row, context, 0)
} }
} }
@ -79,7 +80,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) { return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name)) SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name))
GameRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName)) GamePropertiesRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName))
else -> null else -> null
} }
} }
@ -87,7 +88,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SimpleRow.NAME -> this.name = value as String? ?: "" SimpleRow.NAME -> this.name = value as String? ?: ""
GameRow.SHORT_NAME -> this.shortName = value as String? ?: "" GamePropertiesRow.SHORT_NAME -> this.shortName = value as String? ?: ""
} }
} }

@ -12,11 +12,12 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import java.util.* import java.util.*
open class Location : RealmObject(), NameManageable, RowRepresentable { open class Location : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = Location::class.java override val realmObjectClass: Class<out Identifiable> = Location::class.java

@ -0,0 +1,72 @@
package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.lookupForNameInAllTablesById
import java.util.*
interface PerformanceKey {
val resId: Int?
val value: Int
}
open class Performance() : RealmObject() {
@PrimaryKey
var id: String = UUID.randomUUID().toString()
constructor(
report: StaticReport,
key: PerformanceKey,
name: String? = null,
objectId: String? = null,
customFieldId: String? = null,
value: Double? = null
) : this() {
this.reportId = report.uniqueIdentifier
this.key = key.value
this.name = name
this.objectId = objectId
this.customFieldId = customFieldId
this.value = value
}
var reportId: Int = 0
var key: Int = 0
var name: String? = null
var objectId: String? = null
var customFieldId: String? = null
var value: Double? = null
fun toStaticReport(realm: Realm): StaticReport {
return StaticReport.newInstance(realm, this.reportId, this.customFieldId)
}
fun displayValue(realm: Realm): CharSequence {
this.name?.let { return it }
this.objectId?.let { realm.lookupForNameInAllTablesById(it) }
return NULL_TEXT
}
val stat: Stat
get() {
return Stat.valueByIdentifier(this.key.toInt())
}
val resId: Int?
get() {
return when (this.reportId) {
StaticReport.OptimalDuration.uniqueIdentifier -> LiveOnline.valueByIdentifier(this.key).resId
else -> stat.resId
}
}
}

@ -4,26 +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.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow
import net.pokeranalytics.android.ui.view.rowrepresentable.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 { open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -43,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
@ -68,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) {
PlayerRow.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) {
PlayerRow.NAME -> this.name = value as String? ?: "" PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerRow.SUMMARY -> this.summary = value as String? ?: "" PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerRow.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(PlayerRow.IMAGE)
rows.add(PlayerRow.NAME)
rows.add(PlayerRow.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
} }
/** /**
@ -141,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) {
PlayerRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.name))
PlayerRow.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()) {
@ -212,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()
}
} }

@ -6,6 +6,7 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
@ -30,7 +31,7 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
var name: String = "" var name: String = ""
// The type of display of the report // The type of display of the report
var display: Int = Calculator.Options.Display.TABLE.ordinal var display: Int = ReportDisplay.TABLE.ordinal
/** /**
* A list of statIds to compute * A list of statIds to compute
@ -64,30 +65,30 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
/** /**
* Returns the Options based on the ReportSetup parameters * Returns the Options based on the ReportSetup parameters
*/ */
fun options(realm: Realm): Calculator.Options { fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options {
val stats = this.statIds.map { Stat.valueByIdentifier(it) } val stats = this.statIds.map { Stat.valueByIdentifier(it) }
// Comparison criteria // Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) } val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) } val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
val cfCriteria = customFields.map { it.criteria } val cfCriteria = customFields.map { it.criteria }
val allCriteria = mutableListOf<Criteria>() val allCriteria = mutableListOf<Criteria>()
allCriteria.addAll(criteria) allCriteria.addAll(criteria)
allCriteria.addAll(cfCriteria) allCriteria.addAll(cfCriteria)
return Calculator.Options( return Calculator.Options(
display = Calculator.Options.Display.values()[this.display],
stats = stats, stats = stats,
progressValues = reportDisplay.progressValues,
criterias = allCriteria, criterias = allCriteria,
filter = this.filter, filter = this.filter,
userGenerated = true, userGenerated = true,
reportSetupId = this.id reportSetupId = this.id
) )
} }
// Deletable // Deletable

@ -75,6 +75,10 @@ open class Result : RealmObject(), Filterable {
* Tips * Tips
*/ */
var tips: Double? = null var tips: Double? = null
set(value) {
field = value
this.session?.computeStats()
}
// The transactions associated with the Result, impacting the result // The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList() var transactions: RealmList<Transaction> = RealmList()
@ -98,13 +102,19 @@ open class Result : RealmObject(), Filterable {
/** /**
* Returns 1 if the session is positive * Returns 1 if the session is positive
*/ */
@Ignore val isPositive: Int
val isPositive: Int = if (this.net >= 0.0) 1 else 0 get() {
return if (session?.isTournament() == true) {
if ((this.cashout ?: -1.0) > 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else {
if (this.net >= 0.0) 1 else 0
}
}
// Computes the Net // Computes the Net
private fun computeNet(withBuyin: Boolean? = null) { private fun computeNet(withBuyin: Boolean? = null) {
val transactionsSum = transactions.sumByDouble { it.amount } val transactionsSum = transactions.sumOf { it.amount }
// choose the method to compute the net // choose the method to compute the net
var useBuyin = withBuyin ?: true var useBuyin = withBuyin ?: true
@ -140,7 +150,7 @@ open class Result : RealmObject(), Filterable {
fun computeNumberOfRebuy() { fun computeNumberOfRebuy() {
this.session?.let { this.session?.let {
if (it.isCashGame()) { if (it.isCashGame()) {
it.cgBigBlind?.let { bb -> it.cgBiggestBet?.let { bb ->
if (bb > 0.0) { if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0) this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else { } else {

@ -1,8 +1,6 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import com.crashlytics.android.Crashlytics
import com.github.mikephil.charting.data.Entry
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
@ -13,12 +11,12 @@ import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.Stakes
import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.extensions.SessionState import net.pokeranalytics.android.model.extensions.SessionState
@ -30,26 +28,25 @@ import net.pokeranalytics.android.model.filter.QueryCondition.*
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.SessionSetManager import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.extensions.shortDateTime
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.extensions.toCurrency
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.extensions.toMinutes
import net.pokeranalytics.android.util.extensions.*
import java.text.DateFormat import java.text.DateFormat
import java.text.NumberFormat
import java.text.ParseException
import java.util.* import java.util.*
import java.util.Currency import java.util.Currency
import kotlin.collections.ArrayList
typealias BB = Double typealias BB = Double
open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed, open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Timed,
TimeFilterable, Filterable, DatedBankrollGraphEntry { TimeFilterable, Filterable, DatedBankrollGraphEntry, StakesHolder {
enum class Type(val value: String) { enum class Type(val value: String) {
CASH_GAME("Cash Game"), CASH_GAME("Cash Game"),
@ -80,6 +77,9 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} }
session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal session.type = if (isTournament) Type.TOURNAMENT.ordinal else Type.CASH_GAME.ordinal
session.limit = Limit.NO.ordinal
session.game = realm.where(Game::class.java).equalTo("shortName", "HE").findFirst()
return if (managed) { return if (managed) {
realm.copyToRealm(session) realm.copyToRealm(session)
} else { } else {
@ -99,7 +99,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
AnyLimit::class.java -> "limit" AnyLimit::class.java -> "limit"
AnyTableSize::class.java -> "tableSize" AnyTableSize::class.java -> "tableSize"
AnyTournamentType::class.java -> "tournamentType" AnyTournamentType::class.java -> "tournamentType"
AnyBlind::class.java -> "blinds" AnyStake::class.java -> "cgStakes"
NumberOfTable::class.java -> "numberOfTables" NumberOfTable::class.java -> "numberOfTables"
NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet" NetAmountWon::class.java, NetAmountLost::class.java -> "computableResults.ratedNet"
NumberOfRebuy::class.java -> "result.numberOfRebuy" NumberOfRebuy::class.java -> "result.numberOfRebuy"
@ -110,7 +110,9 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek" AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek"
AnyMonthOfYear::class.java -> "month" AnyMonthOfYear::class.java -> "month"
AnyYear::class.java -> "year" AnyYear::class.java -> "year"
PastDay::class.java, IsToday::class.java, WasYesterday::class.java, WasTodayAndYesterday::class.java, DuringThisYear::class.java, DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate" PastDay::class.java, IsToday::class.java, WasYesterday::class.java,
WasTodayAndYesterday::class.java, DuringThisYear::class.java,
DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate"
StartedFromTime::class.java -> "startDateHourMinuteComponent" StartedFromTime::class.java -> "startDateHourMinuteComponent"
EndedToTime::class.java -> "endDateHourMinuteComponent" EndedToTime::class.java -> "endDateHourMinuteComponent"
Duration::class.java -> "netDuration" Duration::class.java -> "netDuration"
@ -119,7 +121,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
CustomFieldQuery::class.java -> "customFieldEntries.customFields.id" CustomFieldQuery::class.java -> "customFieldEntries.customFields.id"
DateNotNull::class.java -> "startDate" DateNotNull::class.java -> "startDate"
EndDateNotNull::class.java -> "endDate" EndDateNotNull::class.java -> "endDate"
BigBlindNotNull::class.java -> "cgBigBlind" BiggestBetNotNull::class.java -> "cgBiggestBet"
else -> null else -> null
} }
} }
@ -165,9 +167,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
private var startDateHourMinuteComponent: Double? = null private var startDateHourMinuteComponent: Double? = null
get() { get() {
if (field == null && startDate != null) { if (field == null && startDate != null) {
val cal = Calendar.getInstance() startDate?.let { date ->
cal.time = startDate val cal = Calendar.getInstance()
field = cal.hourMinute() cal.time = date
field = cal.hourMinute()
}
} }
return field return field
} }
@ -175,9 +179,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
private var endDateHourMinuteComponent: Double? = null private var endDateHourMinuteComponent: Double? = null
get() { get() {
if (field == null && endDate != null) { if (field == null && endDate != null) {
val cal = Calendar.getInstance() endDate?.let { date ->
cal.time = endDate val cal = Calendar.getInstance()
field = cal.hourMinute() cal.time = date
field = cal.hourMinute()
}
} }
return field return field
} }
@ -248,7 +254,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
override var pauseDate: Date? = null override var pauseDate: Date? = null
set(value) { set(value) {
field = value field = value
this.updateRowRepresentation() // this.updateRowRepresentation()
} }
// The session set containing the sessions, which can contain multiple endedSessions // The session set containing the sessions, which can contain multiple endedSessions
@ -261,12 +267,20 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
override var bankroll: Bankroll? = null override var bankroll: Bankroll? = null
set(value) { set(value) {
field = value field = value
this.formatBlinds() this.generateStakes()
this.updateRowRepresentation() this.computeStats()
// this.updateRowRepresentation()
} }
// The limit type: NL, PL... // The limit type: NL, PL...
var limit: Int? = null var limit: Int? = null
set(value) {
field = if (value != null && value >= 0) {
value
} else {
null
}
}
// The game played during the Session // The game played during the Session
var game: Game? = null var game: Game? = null
@ -279,6 +293,12 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// The number of tables played at the same time // The number of tables played at the same time
var numberOfTables: Int = 1 var numberOfTables: Int = 1
set(value) {
if (value > 0) {
field = value
this.computeStats()
}
}
// The hand histories of the session // The hand histories of the session
@LinkingObjects("session") @LinkingObjects("session")
@ -293,23 +313,43 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// Cash Game // Cash Game
// The small blind value // The small blind value
var cgSmallBlind: Double? = null var cgOldSmallBlind: Double? = null
set(value) { set(value) {
field = value field = value
formatBlinds()
} }
// The big blind value // The big blind value
var cgBigBlind: Double? = null var cgOldBigBlind: Double? = null
set(value) { set(value) {
field = value field = value
this.computeStats() this.computeStats()
formatBlinds()
this.result?.computeNumberOfRebuy() this.result?.computeNumberOfRebuy()
} }
var blinds: String? = null // var blinds: String? = null
private set // private set
var cgAnte: Double? = null
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
this.computeStats()
this.result?.computeNumberOfRebuy()
}
var cgBlinds: String? = null
set(value) {
field = cleanupBlinds(value)
this.generateStakes()
this.defineHighestBet()
this.computeStats()
this.result?.computeNumberOfRebuy()
}
var cgBiggestBet: Double? = null
var cgStakes: String? = null
// Tournament // Tournament
@ -335,8 +375,15 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// The custom fields values // The custom fields values
var customFieldEntries: RealmList<CustomFieldEntry> = RealmList() var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()
// The number of hands played during the sessions
var handsCount: Int? = null
set(value) {
field = value
this.computeStats()
}
fun bankrollHasBeenUpdated() { fun bankrollHasBeenUpdated() {
formatBlinds() this.generateStakes()
} }
/** /**
@ -349,7 +396,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} else if (this.sessionSet != null) { } else if (this.sessionSet != null) {
SessionSetManager.removeFromTimeline(this) SessionSetManager.removeFromTimeline(this)
} }
this.updateRowRepresentation() // this.updateRowRepresentation()
} }
/** /**
@ -382,17 +429,12 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// Stats // Stats
@Ignore
val ONLINE_PLAYER_HANDS_PER_HOUR = 500.0
@Ignore
val LIVE_PLAYER_HANDS_PER_HOUR = 250.0
/** /**
* The net result in big blinds * The net result in big blinds
*/ */
val bbNet: BB val bbNet: BB
get() { get() {
val bb = this.cgBigBlind val bb = this.cgBiggestBet
val result = this.result val result = this.result
return if (bb != null && result != null) { return if (bb != null && result != null) {
result.net / bb result.net / bb
@ -407,12 +449,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
@Ignore @Ignore
var estimatedHands: Double = 0.0 var estimatedHands: Double = 0.0
get() { get() {
this.handsCount?.let {
return it.toDouble()
}
val noh = this.numberOfHandsPerHour val noh = this.numberOfHandsPerHour
val hd = this.hourlyDuration val hd = this.hourlyDuration
return noh * hd return noh * hd
} }
// DatedValue // DatedValue
@Ignore @Ignore
@ -457,11 +501,12 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
val numberOfHandsPerHour: Double val numberOfHandsPerHour: Double
get() { get() {
val tableSize = this.tableSize ?: 9 // 9 is the default table size if null val tableSize = this.tableSize ?: 9 // 9 is the default table size if null
val playerHandsPerHour = if (this.isLive) LIVE_PLAYER_HANDS_PER_HOUR else ONLINE_PLAYER_HANDS_PER_HOUR val config = UserConfig.getConfiguration(this.realm)
return playerHandsPerHour / tableSize.toDouble() val playerHandsPerHour = if (this.isLive) config.liveDealtHandsPerHour else config.onlineDealtHandsPerHour
return this.numberOfTables * playerHandsPerHour / tableSize.toDouble()
} }
private val hourlyRate: Double val hourlyRate: Double
get() { get() {
this.result?.let { result -> this.result?.let { result ->
return result.net / this.hourlyDuration return result.net / this.hourlyDuration
@ -630,22 +675,8 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT
} }
fun getFormattedBlinds(): String { fun getFormattedStakes(): String {
return blinds ?: NULL_TEXT return this.cgStakes?.let { StakesHolder.readableStakes(it) } ?: run { 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
@ -655,14 +686,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
*/ */
fun delete() { fun delete() {
Crashlytics.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()
} }
} else { } else {
Crashlytics.log("Attempt to delete an invalid session") CrashLogging.log("Attempt to delete an invalid session")
} }
} }
@ -681,14 +712,15 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} }
fun duplicate() : Session { fun duplicate(): Session {
val copy = newInstance(this.realm, this.isTournament(), this.bankroll) val copy = newInstance(this.realm, this.isTournament(), this.bankroll)
copy.game = this.game copy.game = this.game
copy.limit = this.limit copy.limit = this.limit
copy.cgSmallBlind = this.cgSmallBlind copy.cgBlinds = this.cgBlinds
copy.cgBigBlind = this.cgBigBlind copy.cgAnte = this.cgAnte
copy.location = this.location
copy.tournamentEntryFee = this.tournamentEntryFee copy.tournamentEntryFee = this.tournamentEntryFee
copy.tournamentFeatures = this.tournamentFeatures copy.tournamentFeatures = this.tournamentFeatures
copy.tournamentName = this.tournamentName copy.tournamentName = this.tournamentName
@ -702,320 +734,45 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
@Ignore @Ignore
override val viewType: Int = RowViewType.ROW_SESSION.ordinal override val viewType: Int = RowViewType.ROW_SESSION.ordinal
// Override to surcharge custom field viewType
override fun viewTypeForPosition(position: Int): Int {
rowRepresentationForCurrentState[position].let {
if (it is CustomField) {
return RowViewType.TITLE_VALUE.ordinal
}
}
return super.viewTypeForPosition(position)
}
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return "Session ${this.creationDate}" return "Session ${this.creationDate}"
} }
@Ignore
private var rowRepresentationForCurrentState: List<RowRepresentable> = mutableListOf()
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
// Headers
when (getState()) {
SessionState.STARTED -> {
rows.add(
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG,
title = getFormattedDuration(),
valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat
)
)
rows.add(SeparatorRow())
}
SessionState.PAUSED -> {
rows.add(
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG,
resId = R.string.pause,
valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat
)
)
rows.add(SeparatorRow())
}
SessionState.FINISHED -> {
rows.add(
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG,
title = getFormattedDuration(),
valueTextFormat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency).textFormat
)
)
rows.add(
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT,
resId = R.string.hour_rate_without_pauses,
valueTextFormat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency = currency).textFormat
)
)
// if (!isTournament()) {
// rows.add(
// CustomizableRowRepresentable(
// RowViewType.HEADER_TITLE_VALUE,
// resId = R.string.bankroll_variation,
// computedStat = ComputedStat(Stat.HOURLY_RATE, 0.0, CurrencyUtils.getCurrency(bankroll))
// )
// )
// }
rows.add(SeparatorRow())
}
else -> {
}
}
// Rows
rows.addAll(SessionRow.getRows(this))
// Add custom fields
realm?.let {
rows.add(SeparatorRow())
rows.addAll(it.sorted<CustomField>())
}
return rows
}
fun updateRowRepresentation() {
this.rowRepresentationForCurrentState = this.updatedRowRepresentationForCurrentState()
}
override fun adapterRows(): List<RowRepresentable>? {
return this.rowRepresentationForCurrentState
}
override fun boolForRow(row: RowRepresentable): Boolean {
return false
}
override fun charSequenceForRow(row: RowRepresentable, context: Context): String {
return when (row) {
SessionRow.BANKROLL -> bankroll?.name ?: NULL_TEXT
SessionRow.BLINDS -> getFormattedBlinds()
SessionRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionRow.CASHED_OUT, SessionRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
SessionRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionRow.GAME -> getFormattedGame()
SessionRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
SessionRow.LOCATION -> location?.name ?: NULL_TEXT
SessionRow.PLAYERS -> tournamentNumberOfPlayers?.toString() ?: NULL_TEXT
SessionRow.POSITION -> result?.tournamentFinalPosition?.toString() ?: NULL_TEXT
SessionRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT
SessionRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT
SessionRow.TIPS -> result?.tips?.toCurrency(currency) ?: NULL_TEXT
SessionRow.TOURNAMENT_TYPE -> {
this.tournamentType?.let {
TournamentType.values()[it].localizedTitle(context)
} ?: run {
NULL_TEXT
}
}
SessionRow.TOURNAMENT_FEATURE -> {
if (tournamentFeatures.size > 2) {
"${tournamentFeatures.subList(0, 2).joinToString {
it.name
}}, ..."
} else if (tournamentFeatures.size > 0) {
tournamentFeatures.joinToString {
it.name
}
} else {
NULL_TEXT
}
}
SessionRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT
SessionRow.HANDS -> this.handHistories?.size.toString()
is CustomField -> {
customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(currency)
}
return NULL_TEXT
}
else -> throw UnmanagedRowRepresentableException("Unmanaged row = $row")
}
}
override fun actionIconForRow(row: RowRepresentable): Int? {
return when (row) {
SessionRow.START_DATE, SessionRow.END_DATE -> {
R.drawable.ic_close
}
else -> null
}
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) {
SessionRow.BANKROLL -> row.editingDescriptors(
mapOf(
"defaultValue" to this.bankroll,
"data" to realm.sorted<Bankroll>() // LiveData.Bankroll.items(realm)
)
)
SessionRow.GAME -> row.editingDescriptors(
mapOf(
"limit" to this.limit,
"defaultValue" to this.game,
"data" to realm.sorted<Game>() //LiveData.Game.items(realm)
)
)
SessionRow.LOCATION -> row.editingDescriptors(
mapOf(
"defaultValue" to this.location,
"data" to realm.sorted<Location>() // LiveData.Location.items(realm)
)
)
SessionRow.TOURNAMENT_FEATURE -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentFeatures,
"data" to realm.sorted<TournamentFeature>() //LiveData.TournamentFeature.items(realm)
)
)
SessionRow.TOURNAMENT_NAME -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentName,
"data" to realm.sorted<TournamentName>() //LiveData.TournamentName.items(realm)
)
)
SessionRow.TOURNAMENT_TYPE -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentType
)
)
SessionRow.TABLE_SIZE -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tableSize
)
)
SessionRow.BLINDS -> row.editingDescriptors(
mapOf(
"sb" to cgSmallBlind?.round(),
"bb" to cgBigBlind?.round()
)
)
SessionRow.BUY_IN -> row.editingDescriptors(
mapOf(
"bb" to cgBigBlind,
"fee" to this.tournamentEntryFee,
"ratedBuyin" to result?.buyin
)
)
SessionRow.BREAK_TIME -> row.editingDescriptors(mapOf())
SessionRow.CASHED_OUT, SessionRow.PRIZE -> row.editingDescriptors(
mapOf(
"defaultValue" to result?.cashout
)
)
SessionRow.NET_RESULT -> row.editingDescriptors(
mapOf(
"defaultValue" to result?.netResult
)
)
SessionRow.COMMENT -> row.editingDescriptors(
mapOf(
"defaultValue" to this.comment
)
)
SessionRow.INITIAL_BUY_IN -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentEntryFee
)
)
SessionRow.PLAYERS -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentNumberOfPlayers
)
)
SessionRow.POSITION -> row.editingDescriptors(
mapOf(
"defaultValue" to this.result?.tournamentFinalPosition
)
)
SessionRow.TIPS -> row.editingDescriptors(
mapOf(
"sb" to cgSmallBlind?.round(),
"bb" to cgBigBlind?.round(),
"tips" to result?.tips
)
)
is CustomField -> {
row.editingDescriptors(
when (row.type) {
CustomField.Type.LIST.uniqueIdentifier -> mapOf(
"defaultValue" to customFieldEntries.find { it.customField?.id == row.id }?.value,
"data" to row.entries
)
else -> mapOf(
"defaultValue" to customFieldEntries.find { it.customField?.id == row.id }?.numericValue
)
}
)
}
else -> null
}
}
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SessionRow.BANKROLL -> bankroll = value as Bankroll? SessionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
SessionRow.BLINDS -> if (value is ArrayList<*>) { SessionPropertiesRow.STAKES -> if (value is Stakes) {
cgSmallBlind = try { if (value.ante != null) {
(value[0] as String? ?: "0").toDouble() this.cgAnte = value.ante
} catch (e: Exception) {
null
} }
if (value.blinds != null) {
cgBigBlind = try { this.cgBlinds = value.blinds
(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) {
cgSmallBlind = null this.cgBlinds = null
cgBigBlind = null this.cgAnte = null
} }
SessionRow.BREAK_TIME -> { SessionPropertiesRow.BREAK_TIME -> {
this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000 this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000
} }
SessionRow.BUY_IN -> { SessionPropertiesRow.BUY_IN -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.buyin = value as Double? localResult.buyin = value as Double?
this.updateRowRepresentation()
} }
SessionRow.CASHED_OUT, SessionRow.PRIZE -> { SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.cashout = value as Double? localResult.cashout = value as Double?
} }
SessionRow.NET_RESULT -> { SessionPropertiesRow.NET_RESULT -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.netResult = value as Double? localResult.netResult = value as Double?
} }
SessionRow.COMMENT -> comment = value as String? ?: "" SessionPropertiesRow.COMMENT -> comment = value as String? ?: ""
SessionRow.END_DATE -> if (value is Date?) { SessionPropertiesRow.END_DATE -> if (value is Date?) {
this.endDate = value this.endDate = value
} }
SessionRow.GAME -> { SessionPropertiesRow.GAME -> {
if (value is ArrayList<*>) { if (value is ArrayList<*>) {
limit = try { limit = try {
(value[0] as Int?) (value[0] as Int?)
@ -1034,18 +791,18 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
game = null game = null
} }
} }
SessionRow.INITIAL_BUY_IN -> { SessionPropertiesRow.INITIAL_BUY_IN -> {
this.tournamentEntryFee = (value as Double?) this.tournamentEntryFee = (value as Double?)
} }
SessionRow.LOCATION -> location = value as Location? SessionPropertiesRow.LOCATION -> location = value as Location?
SessionRow.PLAYERS -> { SessionPropertiesRow.PLAYERS -> {
if (value is Double) { if (value is Double) {
this.tournamentNumberOfPlayers = value.toInt() this.tournamentNumberOfPlayers = value.toInt()
} else { } else {
this.tournamentNumberOfPlayers = null this.tournamentNumberOfPlayers = null
} }
} }
SessionRow.POSITION -> { SessionPropertiesRow.POSITION -> {
val localResult = if (result != null) result as Result else realm.createObject(Result::class.java) val localResult = if (result != null) result as Result else realm.createObject(Result::class.java)
if (value is Double) { if (value is Double) {
localResult.tournamentFinalPosition = value.toInt() localResult.tournamentFinalPosition = value.toInt()
@ -1054,17 +811,17 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} }
result = localResult result = localResult
} }
SessionRow.START_DATE -> if (value is Date) { SessionPropertiesRow.START_DATE -> if (value is Date) {
this.startDate = value this.startDate = value
} }
SessionRow.TABLE_SIZE -> tableSize = value as Int? SessionPropertiesRow.TABLE_SIZE -> tableSize = value as Int?
SessionRow.TIPS -> { SessionPropertiesRow.TIPS -> {
val localResult = getOrCreateResult() val localResult = getOrCreateResult()
localResult.tips = value as Double? localResult.tips = value as Double?
} }
SessionRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName? SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName = value as TournamentName?
SessionRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal SessionPropertiesRow.TOURNAMENT_TYPE -> tournamentType = (value as TournamentType?)?.ordinal
SessionRow.TOURNAMENT_FEATURE -> { SessionPropertiesRow.TOURNAMENT_FEATURE -> {
value?.let { value?.let {
tournamentFeatures = RealmList() tournamentFeatures = RealmList()
@ -1073,6 +830,8 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
tournamentFeatures.removeAll(this.tournamentFeatures) tournamentFeatures.removeAll(this.tournamentFeatures)
} }
} }
SessionPropertiesRow.HANDS_COUNT -> handsCount = (value as Double?)?.toInt()
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables = (value as Double?)?.toInt() ?: 1
is CustomField -> { is CustomField -> {
customFieldEntries.filter { it.customField?.id == row.id }.let { customFieldEntries.filter { it.customField?.id == row.id }.let {
customFieldEntries.removeAll(it) customFieldEntries.removeAll(it)
@ -1138,7 +897,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
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
@ -1160,14 +919,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
override fun legendValues( override fun legendValues(
stat: Stat, stat: Stat,
entry: Entry, total: Double,
style: GraphFragment.Style, style: Graph.Style,
groupName: String, groupName: String,
context: Context context: Context
): LegendContent { ): LegendContent {
when (style) { when (style) {
GraphFragment.Style.MULTILINE -> { Graph.Style.MULTILINE -> {
val secondTitle = stat.localizedTitle(context) val secondTitle = stat.localizedTitle(context)
val entryValue = this.formattedValue(stat) val entryValue = this.formattedValue(stat)
@ -1193,7 +952,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
DefaultLegendValues(this.entryTitle(context), left, right) DefaultLegendValues(this.entryTitle(context), left, right)
} }
else -> { else -> {
super<Timed>.legendValues(stat, entry, style, groupName, context) super<Timed>.legendValues(stat, total, style, groupName, context)
} }
} }
} }
@ -1206,4 +965,113 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = Session::class.java override val realmObjectClass: Class<out Identifiable> = Session::class.java
fun charSequenceForRow(row: RowRepresentable, context: Context): String {
return when (row) {
SessionPropertiesRow.BANKROLL -> bankroll?.name ?: NULL_TEXT
SessionPropertiesRow.STAKES -> getFormattedStakes()
SessionPropertiesRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
SessionPropertiesRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.CASHED_OUT, SessionPropertiesRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
SessionPropertiesRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.GAME -> getFormattedGame()
SessionPropertiesRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.LOCATION -> location?.name ?: NULL_TEXT
SessionPropertiesRow.PLAYERS -> tournamentNumberOfPlayers?.toString() ?: NULL_TEXT
SessionPropertiesRow.POSITION -> result?.tournamentFinalPosition?.toString() ?: NULL_TEXT
SessionPropertiesRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT
SessionPropertiesRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT
SessionPropertiesRow.TIPS -> result?.tips?.toCurrency(currency) ?: NULL_TEXT
SessionPropertiesRow.TOURNAMENT_TYPE -> {
this.tournamentType?.let {
TournamentType.values()[it].localizedTitle(context)
} ?: run {
NULL_TEXT
}
}
SessionPropertiesRow.TOURNAMENT_FEATURE -> {
if (tournamentFeatures.size > 2) {
"${tournamentFeatures.subList(0, 2).joinToString {
it.name
}}, ..."
} else if (tournamentFeatures.size > 0) {
tournamentFeatures.joinToString {
it.name
}
} else {
NULL_TEXT
}
}
SessionPropertiesRow.TOURNAMENT_NAME -> tournamentName?.name ?: NULL_TEXT
SessionPropertiesRow.HANDS -> this.handHistories?.size.toString()
SessionPropertiesRow.HANDS_COUNT -> this.handsCountFormatted(context)
SessionPropertiesRow.NUMBER_OF_TABLES -> this.numberOfTables.toString()
is CustomField -> {
customFieldEntries.find { it.customField?.id == row.id }?.let { customFieldEntry ->
return customFieldEntry.getFormattedValue(currency)
}
return NULL_TEXT
}
else -> throw UnmanagedRowRepresentableException("Unmanaged row = $row")
}
}
private fun handsCountFormatted(context: Context): String {
return this.handsCount?.toString() ?:
"${estimatedHands.toInt()} (${context.getString(R.string.estimated)})"
}
fun clearBuyinCashedOut() {
this.result?.buyin = null
this.result?.cashout = null
}
fun clearNetResult() {
this.result?.netResult = null
}
private fun cleanupBlinds(blinds: String?): String? {
if (blinds == null) {
return null
}
val blindValues = blinds.split(BLIND_SEPARATOR).mapNotNull {
try {
NumberFormat.getInstance().parse(it)
} catch (e: ParseException) {
null
}
}
return if (blindValues.isNotEmpty()) {
blindValues.joinToString(BLIND_SEPARATOR)
} else {
null
}
}
/// StakesHolder
override val ante: Double?
get() { return this.cgAnte }
override val blinds: String?
get() { return this.cgBlinds }
override val biggestBet: Double?
get() { return this.cgBiggestBet }
override val stakes: String?
get() { return this.cgStakes }
override fun setHolderStakes(stakes: String?) {
this.cgStakes = stakes
}
override fun setHolderBiggestBet(biggestBet: Double?) {
this.cgBiggestBet = biggestBet
}
} }

@ -61,9 +61,9 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override var netDuration: Long = 0L override var netDuration: Long = 0L
fun computeStats() { fun computeStats() {
this.ratedNet = this.sessions?.sumByDouble { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0 this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumByDouble { it.estimatedHands } ?: 0.0 this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumByDouble { it.bbNet } ?: 0.0 this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L
} }

@ -8,21 +8,21 @@ import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.CountableUsage import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
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.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentFeatureRow import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentFeatureRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, UsageCountable {
CountableUsage {
companion object { companion object {
val rowRepresentation : List<RowRepresentable> by lazy { val rowRepresentation : List<RowRepresentable> by lazy {

@ -13,14 +13,16 @@ import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
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.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentNameRow import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentNameRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable { open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource,
RowUpdatable, RowRepresentable {
companion object { companion object {
val rowRepresentation : List<RowRepresentable> by lazy { val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()

@ -1,7 +1,6 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import com.github.mikephil.charting.data.Entry
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
@ -12,25 +11,21 @@ import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.DefaultLegendValues import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.LegendContent import net.pokeranalytics.android.ui.view.rows.TransactionPropertiesRow
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionRow
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.math.abs
open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageable, StaticRowRepresentableDataSource, TimeFilterable,
open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, TimeFilterable,
Filterable, DatedBankrollGraphEntry { Filterable, DatedBankrollGraphEntry {
companion object { companion object {
fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double, comment: String? = null): Transaction { fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double, comment: String? = null, destination: Bankroll? = null, transferRate: Double? = null): Transaction {
val transaction = realm.copyToRealm(Transaction()) val transaction = realm.copyToRealm(Transaction())
transaction.date = date ?: Date() transaction.date = date ?: Date()
@ -38,14 +33,14 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
transaction.type = type transaction.type = type
transaction.bankroll = bankroll transaction.bankroll = bankroll
transaction.comment = comment ?: "" transaction.comment = comment ?: ""
transaction.destination = destination
transaction.transferRate = transferRate
return transaction if (destination != null) { // we make sure transfers are negative
} transaction.amount = abs(amount) * -1
}
val rowRepresentation: List<RowRepresentable> by lazy { return transaction
val rows = ArrayList<RowRepresentable>()
rows.addAll(TransactionRow.values())
rows
} }
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? { fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
@ -78,6 +73,13 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
// The amount of the transaction // The amount of the transaction
override var amount: Double = 0.0 override var amount: Double = 0.0
set(value) {
field = value
computeRatedAmount()
}
// The amount of the transaction
var ratedAmount: Double = 0.0
// The date of the transaction // The date of the transaction
override var date: Date = Date() override var date: Date = Date()
@ -92,6 +94,12 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
// A user comment // A user comment
var comment: String = "" var comment: String = ""
// The destination Bankroll of a transfer
var destination: Bankroll? = null
// The rate of the transfer when bankrolls are in different currencies
var transferRate: Double? = null
// Timed interface // Timed interface
override var dayOfWeek: Int? = null override var dayOfWeek: Int? = null
override var month: Int? = null override var month: Int? = null
@ -101,18 +109,34 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
@Ignore @Ignore
override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal
enum class Field(val identifier: String) {
RATED_AMOUNT("ratedAmount")
}
fun computeRatedAmount() {
val rate = this.bankroll?.currency?.rate ?: 1.0
this.ratedAmount = rate * this.amount
}
val displayAmount: Double
get() { // for transfers we want to show a positive value (in the feed for instance)
return if (this.destination == null) { this.amount } else { abs(this.amount) }
}
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
TransactionRow.BANKROLL -> bankroll = value as Bankroll? TransactionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
TransactionRow.TYPE -> type = value as TransactionType? TransactionPropertiesRow.TYPE -> type = value as TransactionType?
TransactionRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble() TransactionPropertiesRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionRow.COMMENT -> comment = value as String? ?: "" TransactionPropertiesRow.COMMENT -> comment = value as String? ?: ""
TransactionRow.DATE -> date = value as Date? ?: Date() TransactionPropertiesRow.DATE -> date = value as Date? ?: Date()
TransactionPropertiesRow.DESTINATION -> destination = value as? Bankroll
TransactionPropertiesRow.RATE -> transferRate = (value as String?)?.toDouble()
} }
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation return rowRepresentation()
} }
override fun isValidForSave(): Boolean { override fun isValidForSave(): Boolean {
@ -148,6 +172,24 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
return SaveValidityStatus.VALID return SaveValidityStatus.VALID
} }
fun rowRepresentation(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
when (this.type?.kind) {
TransactionType.Value.TRANSFER.uniqueIdentifier -> {
if (this.bankroll != null && this.bankroll?.currency?.code != this.destination?.currency?.code) {
rows.addAll(TransactionPropertiesRow.transferRateRows)
} else {
rows.addAll(TransactionPropertiesRow.transferRows)
}
}
else -> {
rows.addAll(TransactionPropertiesRow.baseRows)
}
}
return rows
}
// GraphIdentifiableEntry // GraphIdentifiableEntry
@Ignore @Ignore
@ -163,14 +205,14 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
override fun legendValues( override fun legendValues(
stat: Stat, stat: Stat,
entry: Entry, total: Double,
style: GraphFragment.Style, style: Graph.Style,
groupName: String, groupName: String,
context: Context context: Context
): LegendContent { ): LegendContent {
val entryValue = this.formattedValue(stat) val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(entry.y.toDouble(), currency = null) val totalStatValue = stat.textFormat(total, currency = null)
val leftName = context.getString(R.string.amount) val leftName = context.getString(R.string.amount)
return DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName) return DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName)
} }

@ -13,16 +13,16 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.Localizable import net.pokeranalytics.android.ui.view.Localizable
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.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionTypeRow import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TransactionTypePropertiesRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable import net.pokeranalytics.android.util.enumerations.IntSearchable
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource,
CountableUsage { UsageCountable {
enum class Value(override var uniqueIdentifier: Int, val additive: Boolean) : IntIdentifiable, Localizable { enum class Value(override var uniqueIdentifier: Int, val additive: Boolean) : IntIdentifiable, Localizable {
@ -30,7 +30,9 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
DEPOSIT(1, true), DEPOSIT(1, true),
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),
EXPENSE(6, false); // not created by default, only used for poker base import atm
companion object : IntSearchable<Value> { companion object : IntSearchable<Value> {
@ -47,6 +49,8 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
BONUS -> R.string.bonus BONUS -> R.string.bonus
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
EXPENSE -> R.string.expense
} }
} }
@ -56,7 +60,7 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
val rowRepresentation: List<RowRepresentable> by lazy { val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME) rows.add(SimpleRow.NAME)
rows.addAll(TransactionTypeRow.values()) rows.addAll(TransactionTypePropertiesRow.values())
rows rows
} }
@ -68,6 +72,10 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
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) {
@ -131,7 +139,7 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
override fun boolForRow(row: RowRepresentable): Boolean { override fun boolForRow(row: RowRepresentable): Boolean {
return when (row) { return when (row) {
TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive
else -> super.boolForRow(row) else -> super.boolForRow(row)
} }
} }
@ -143,7 +151,7 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SimpleRow.NAME -> this.name = value as String? ?: "" SimpleRow.NAME -> this.name = value as String? ?: ""
TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false
} }
} }

@ -0,0 +1,41 @@
package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
open class UserConfig : RealmObject() {
companion object {
fun getConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config
}
return UserConfig()
}
}
@PrimaryKey
var id = UUID.randomUUID().toString()
var liveDealtHandsPerHour: Int = 250
var onlineDealtHandsPerHour: Int = 500
var transactionTypeIds: String = ""
fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) {
this.transactionTypeIds = transactionTypes.joinToString(UUID_SEPARATOR) { it.id }
}
fun transactionTypes(realm: Realm): List<TransactionType> {
val ids = this.transactionTypeIds.split(UUID_SEPARATOR)
return ids.mapNotNull { realm.findById(it) }
}
}

@ -14,6 +14,7 @@ import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
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 timber.log.Timber
interface CardProperty interface CardProperty
@ -24,11 +25,10 @@ fun List<Card>.formatted(context: Context) : CharSequence? {
var span: CharSequence? = null var span: CharSequence? = null
this.forEach { this.forEach {
val formatted = it.formatted(context) val formatted = it.formatted(context)
if (span == null) { span = if (span == null) {
span = formatted formatted
} } else {
else { TextUtils.concat(span, formatted)
span = TextUtils.concat(span, formatted)
} }
} }
return span return span
@ -95,6 +95,10 @@ open class Card : RealmObject() {
companion object { companion object {
val baseSuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER)
}
val displaySuits: List<Suit> by lazy { val displaySuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED) listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED)
} }
@ -240,9 +244,49 @@ open class Card : RealmObject() {
} }
} }
fun copy(): Card {
val card = Card()
card.value = this.value
card.suit = this.suit
return card
}
val isWildCard: Boolean val isWildCard: Boolean
get() { get() {
return this.value == null || this.suit == null || this.suit == Suit.UNDEFINED return this.value == null || this.isSuitWildCard
} }
} val isSuitWildCard: Boolean
get() {
return this.suit == null || this.suit == Suit.UNDEFINED
}
}
fun List<Card>.putSuits(usedCards: MutableList<Card>) : List<Card> {
fun getLeastUsedValidSuit(value: Int, usedCards: MutableList<Card>, addedCards: List<Card>): Card.Suit {
val availableSuits = Card.Suit.baseSuits.toMutableList()
usedCards.filter { it.value == value }.forEach { availableSuits.remove(it.suit) }
val currentCards = this.drop(addedCards.size) + addedCards
val cardsPerSuit = availableSuits.associateWith { suit ->
currentCards.filter { it.suit == suit }
}
return cardsPerSuit.minByOrNull { it.value.count() }!!.key
}
val list = mutableListOf<Card>()
this.forEach { cardToClone ->
val card = cardToClone.copy()
if (card.value != null && card.isSuitWildCard) {
card.suit = getLeastUsedValidSuit(card.value!!, usedCards, list)
usedCards.add(card)
Timber.d("Selected suit for wildcard: ${card.suit?.value}, value = ${card.value}")
}
list.add(card)
}
return list
}

@ -15,10 +15,8 @@ import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.handhistory.HandSetup import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.TimeFilterable
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow
@ -28,13 +26,14 @@ import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.addLineReturn import net.pokeranalytics.android.util.extensions.addLineReturn
import net.pokeranalytics.android.util.extensions.formatted import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.fullDate import net.pokeranalytics.android.util.extensions.fullDate
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean) data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean)
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable, open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable,
CardHolder, Comparator<PositionAmount> { CardHolder, Comparator<PositionAmount>, StakesHolder {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -63,34 +62,67 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
/*** /***
* The small blind * The small blind
*/ */
var smallBlind: Double? = null var oldSmallBlind: Double? = null
set(value) { set(value) {
field = value field = value
if (this.bigBlind == null && value != null) { // if (this.bigBlind == null && value != null) {
this.bigBlind = value * 2 // this.bigBlind = value * 2
} // }
} }
/*** /***
* The big blind * The big blind
*/ */
var bigBlind: Double? = null var oldBigBlind: Double? = null
set(value) { set(value) {
field = value field = value
if (this.smallBlind == null && value != null) { // if (this.smallBlind == null && value != null) {
this.smallBlind = value / 2 // this.smallBlind = value / 2
} // }
} }
/***
* Big blind ante
*/
var bigBlindAnte: Boolean = false
/*** /***
* The ante * The ante
*/ */
var ante: Double = 0.0 override var ante: Double? = 0.0
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
/*** /***
* Big blind ante * The blinds
*/ */
var bigBlindAnte: Boolean = false override var blinds: String? = null
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
override var biggestBet: Double? = null
/***
* The coded stakes
*/
override var stakes: String? = null
override val bankroll: Bankroll?
get() { return this.session?.bankroll }
override fun setHolderStakes(stakes: String?) {
this.stakes = stakes
}
override fun setHolderBiggestBet(biggestBet: Double?) {
this.biggestBet = biggestBet
}
/*** /***
* Number of players in the hand * Number of players in the hand
@ -158,13 +190,15 @@ 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.smallBlind?.let { this.smallBlind = it } handSetup.ante?.let { this.ante = it }
handSetup.bigBlind?.let { this.bigBlind = it } handSetup.blinds?.let { this.blinds = it }
this.session = handSetup.session this.session = handSetup.session
this.date = this.session?.handHistoryAutomaticDate ?: Date() this.date = this.session?.handHistoryAutomaticDate ?: Date()
@ -179,18 +213,32 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.actions.clear() this.actions.clear()
this.addAction(0, Action.Type.POST_SB, this.smallBlind) var blindValues = this.blindValues
this.addAction(1, Action.Type.POST_BB, this.bigBlind) if (blindValues.isNotEmpty()) {
blindValues.forEachIndexed { index, blind ->
// var lastStraddler: Int? = null val action = when(index) {
0 -> Action.Type.POST_SB
1 -> Action.Type.POST_BB
else -> null
}
action?.let { this.addAction(index, action, blind) }
}
} else {
this.addAction(0, Action.Type.POST_SB, this.oldSmallBlind)
this.addAction(1, Action.Type.POST_BB, this.oldBigBlind)
}
blindValues = blindValues.drop(2)
val positions = Position.positionsPerPlayers(this.numberOfPlayers) val positions = Position.positionsPerPlayers(this.numberOfPlayers)
handSetup.straddlePositions.forEach { position -> // position are sorted here handSetup.straddlePositions.forEachIndexed { index, position -> // position are sorted here
val positionIndex = positions.indexOf(position) val positionIndex = positions.indexOf(position)
this.addAction(positionIndex, Action.Type.STRADDLE) val amount = if (index < blindValues.size) { blindValues[index] } else null
// lastStraddler = positionIndex this.addAction(positionIndex, Action.Type.STRADDLE, amount)
} }
// var lastStraddler: Int? = null
// val totalActions = this.actions.size // val totalActions = this.actions.size
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions // val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions
@ -236,9 +284,9 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val anteSum: Double val anteSum: Double
get() { get() {
return if (bigBlindAnte) { return if (bigBlindAnte) {
this.bigBlind ?: 0.0 this.biggestBet ?: 0.0
} else { } else {
this.ante * this.numberOfPlayers (this.ante ?: 0.0) * this.numberOfPlayers
} }
} }
@ -286,7 +334,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val sortedActions = this.sortedActions val sortedActions = this.sortedActions
val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index
?: sortedActions.size ?: sortedActions.size
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumByDouble { it.effectiveAmount } return this.anteSum + sortedActions.take(firstIndexOfStreet).sumOf { it.effectiveAmount }
} }
@Ignore @Ignore
@ -302,14 +350,20 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}" val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}"
val firstLineComponents = mutableListOf(this.date.fullDate(), players) val firstLineComponents = mutableListOf(this.date.fullDate(), players)
this.smallBlind?.let { sb -> this.blinds?.let { firstLineComponents.add(it) }
this.bigBlind?.let { bb ->
firstLineComponents.add("${sb.formatted}/${bb.formatted}") // this.smallBlind?.let { sb ->
// this.bigBlind?.let { bb ->
// firstLineComponents.add("${sb.formatted}/${bb.formatted}")
// }
// }
this.ante?.let {
if (it > 0.0) {
firstLineComponents.add("ante ${this.ante}")
} }
} }
if (this.ante > 0.0) {
firstLineComponents.add("ante ${this.ante}")
}
string = string.plus(firstLineComponents.joinToString(" - ")) string = string.plus(firstLineComponents.joinToString(" - "))
string = string.addLineReturn(2) string = string.addLineReturn(2)
@ -328,26 +382,26 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
// Actions per street // Actions per street
val sortedActions = this.actions.sortedBy { it.index } val sortedActions = this.actions.sortedBy { it.index }
// val actionList = ActionList() // Remove SUMMARY
// actionList.load(this) Street.values().dropLast(1).forEach { street ->
Street.values().forEach { street ->
string = string.addLineReturn(2) string = string.addLineReturn(2)
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex) val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex)
if (streetActions.isNotEmpty()) {
val streetCards = this.cardsForStreet(street)
// we want to show streets: with actions or when an allin is called
if (streetCards.size == street.totalBoardCards || streetActions.isNotEmpty()) {
val streetItems = mutableListOf<CharSequence>(context.getString(street.resId)) val streetItems = mutableListOf<CharSequence>(context.getString(street.resId))
val potSize = this.potSizeForStreet(street) val potSize = this.potSizeForStreet(street)
if (potSize > 0) { if (potSize > 0) {
// streetItems.add(context.getString(R.string.pot_size))
streetItems.add("(" + potSize.formatted + ")") streetItems.add("(" + potSize.formatted + ")")
} }
string = string.plus(streetItems.joinToString(" ")) string = string.plus(streetItems.joinToString(" "))
val streetCards = this.cardsForStreet(street)
if (streetCards.isNotEmpty()) { if (streetCards.isNotEmpty()) {
string = string.addLineReturn() string = string.addLineReturn()
string = string.plus(streetCards.formatted(context) ?: "") string = string.plus(streetCards.formatted(context) ?: "")
@ -361,6 +415,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
string = string.plus(localizedAction(action, context)) string = string.plus(localizedAction(action, context))
string = string.addLineReturn() string = string.addLineReturn()
} }
} }
} }
@ -371,12 +426,12 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
fun anteForPosition(position: Position): Double { fun anteForPosition(position: Position): Double {
return if (this.bigBlindAnte) { return if (this.bigBlindAnte) {
if (position == Position.BB) { if (position == Position.BB) {
this.bigBlind ?: 0.0 this.biggestBet ?: 0.0
} else { } else {
0.0 0.0
} }
} else { } else {
this.ante this.ante ?: 0.0
} }
} }
@ -392,7 +447,11 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroString = context.getString(R.string.hero) val heroString = context.getString(R.string.hero)
playerItems.add("- $heroString") playerItems.add("- $heroString")
} }
playerItems.add("[${playerSetup.cards.formatted(context)}]")
if (playerSetup.cards.isNotEmpty()) {
playerItems.add("[${playerSetup.cards.formatted(context)}]")
}
playerSetup.stack?.let { stack -> playerSetup.stack?.let { stack ->
playerItems.add("- $stack") playerItems.add("- $stack")
} }
@ -420,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
} }
@ -504,6 +568,8 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
this.winnerPots.clear() this.winnerPots.clear()
this.winnerPots.addAll(wonPots) this.winnerPots.addAll(wonPots)
Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map {it.position}} ")
} }
/*** /***
@ -520,14 +586,14 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position } val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position }
if (allinPositions.isEmpty()) { if (allinPositions.isEmpty()) {
runningPotAmount += streetActions.sumByDouble { it.effectiveAmount } runningPotAmount += streetActions.sumOf { it.effectiveAmount }
} else { } else {
val amountsPerPosition = mutableListOf<PositionAmount>() val amountsPerPosition = mutableListOf<PositionAmount>()
// get all committed amounts for the street by player, by allin // get all committed amounts for the street by player, by allin
this.positionIndexes.map { position -> this.positionIndexes.map { position ->
val playerActions = streetActions.filter { it.position == position } val playerActions = streetActions.filter { it.position == position }
val sum = playerActions.sumByDouble { it.effectiveAmount } val sum = playerActions.sumOf { it.effectiveAmount }
amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position))) amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position)))
} }
amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence
@ -580,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
}
} }
} }
@ -606,17 +675,32 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
*/ */
private fun compareHands(activePositions: List<Int>): List<Int> { private fun compareHands(activePositions: List<Int>): List<Int> {
val usedFullCards = this.allFullCards.toMutableList()
val noWildCardBoard = this.board.putSuits(usedFullCards)
val noWildCardHands = activePositions.map {
this.playerSetupForPosition(it)?.cards?.putSuits(usedFullCards)
}
// Evaluate all hands // Evaluate all hands
val results = activePositions.map { val results = noWildCardHands.map { cards ->
this.playerSetupForPosition(it)?.cards?.let { hand -> cards?.let { hand ->
EvaluatorBridge.playerHand(hand, this.board) EvaluatorBridge.playerHand(hand, noWildCardBoard)
} ?: run { } ?: run {
Int.MAX_VALUE Int.MAX_VALUE
} }
} }
// Evaluate all hands
// val results = activePositions.map {
// this.playerSetupForPosition(it)?.cards?.let { hand ->
// EvaluatorBridge.playerHand(hand, this.board)
// } ?: run {
// Int.MAX_VALUE
// }
// }
// Check who has best score (EvaluatorBridge gives a lowest score for a better hand) // Check who has best score (EvaluatorBridge gives a lowest score for a better hand)
return results.min()?.let { best -> return results.minOrNull()?.let { best ->
val winners = mutableListOf<Int>() val winners = mutableListOf<Int>()
results.forEachIndexed { index, i -> results.forEachIndexed { index, i ->
if (i == best && i != Int.MAX_VALUE) { if (i == best && i != Int.MAX_VALUE) {
@ -659,4 +743,33 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return boardHasWildCard || playerCardHasWildCard return boardHasWildCard || playerCardHasWildCard
} }
private val allFullCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
this.playerSetups.forEach {
cards.addAll(it.cards)
}
return cards.filter { !it.isWildCard }
}
val allSuitWildCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
this.playerSetups.forEach {
cards.addAll(it.cards)
}
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
}
}
} }

@ -0,0 +1,14 @@
package net.pokeranalytics.android.model.realm.rows
import android.content.Context
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.ui.view.RowRepresentable
class BankrollRow(var bankroll: Bankroll) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return this.bankroll.name
}
}

@ -0,0 +1,57 @@
package net.pokeranalytics.android.model.realm.rows
import android.content.Context
import android.text.InputType
import io.realm.RealmList
import io.realm.annotations.Ignore
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
class CustomFieldRow(var customField: CustomField) : RowRepresentable {
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.customField.name
}
override fun getDisplayName(context: Context): String {
return this.customField.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.customField.type) {
CustomField.Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.customField.type) {
CustomField.Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
}

@ -1,11 +0,0 @@
package net.pokeranalytics.android.model.retrofit
import com.google.gson.annotations.SerializedName
/**
* CurrencyCode Converter mapping class
*/
class CurrencyConverterValue {
@SerializedName("val")
var value: Double? = 0.0
}

@ -11,15 +11,18 @@ class DataUtils {
companion object { companion object {
/** /**
* Returns true if the provided parameters doesn't correspond to an existing session * Returns the number of sessions corresponding to the provided parameters
*/ */
fun sessionCount(realm: Realm, startDate: Date, endDate: Date, net: Double): Int { fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int {
val sessions = realm.where(Session::class.java) var sessionQuery = realm.where(Session::class.java)
.equalTo("startDate", startDate) .equalTo("startDate", startDate)
.equalTo("endDate", endDate)
.equalTo("result.net", net) .equalTo("result.net", net)
.findAll()
return sessions.size endDate?.let {
sessionQuery = sessionQuery.equalTo("endDate", it)
}
return sessionQuery.findAll().size
} }
/** /**

@ -5,7 +5,7 @@ import io.realm.Realm
import io.realm.Sort import io.realm.Sort
import net.pokeranalytics.android.model.realm.Location import net.pokeranalytics.android.model.realm.Location
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
/** /**
* Returns all significant parameters concatenated in a String * Returns all significant parameters concatenated in a String
@ -24,24 +24,24 @@ private fun Session.parameterRepresentation(context: Context): String {
/** /**
* Returns a list of fields used to determine which kind of session is favorite * Returns a list of fields used to determine which kind of session is favorite
*/ */
private fun Session.significantFields(): List<SessionRow> { private fun Session.significantFields(): List<SessionPropertiesRow> {
when (this.type) { when (this.type) {
Session.Type.TOURNAMENT.ordinal -> { Session.Type.TOURNAMENT.ordinal -> {
return listOf( return listOf(
SessionRow.GAME, SessionPropertiesRow.GAME,
SessionRow.INITIAL_BUY_IN, SessionPropertiesRow.INITIAL_BUY_IN,
SessionRow.BANKROLL, SessionPropertiesRow.BANKROLL,
SessionRow.TABLE_SIZE, SessionPropertiesRow.TABLE_SIZE,
SessionRow.TOURNAMENT_NAME, SessionPropertiesRow.TOURNAMENT_NAME,
SessionRow.TOURNAMENT_TYPE SessionPropertiesRow.TOURNAMENT_TYPE
) )
} }
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {
return listOf( return listOf(
SessionRow.GAME, SessionPropertiesRow.GAME,
SessionRow.BLINDS, SessionPropertiesRow.STAKES,
SessionRow.BANKROLL, SessionPropertiesRow.BANKROLL,
SessionRow.TABLE_SIZE SessionPropertiesRow.TABLE_SIZE
) )
} }
} }
@ -75,8 +75,7 @@ class FavoriteSessionFinder {
*/ */
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) { fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) {
val favoriteSession = val favoriteSession = favoriteSession(session.type, location, session.realm, context)
favoriteSession(session.type, location, session.realm, context)
favoriteSession?.let { fav -> favoriteSession?.let { fav ->
@ -87,8 +86,8 @@ class FavoriteSessionFinder {
when (session.type) { when (session.type) {
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {
session.cgSmallBlind = fav.cgSmallBlind session.cgAnte = fav.cgAnte
session.cgBigBlind = fav.cgBigBlind session.cgBlinds = fav.cgBlinds
} }
Session.Type.TOURNAMENT.ordinal -> { Session.Type.TOURNAMENT.ordinal -> {
session.tournamentEntryFee = fav.tournamentEntryFee session.tournamentEntryFee = fav.tournamentEntryFee

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

@ -8,8 +8,8 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.activity_color_picker.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.ActivityColorPickerBinding
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
class ColorPickerActivity : BaseActivity() { class ColorPickerActivity : BaseActivity() {
@ -33,15 +33,17 @@ class ColorPickerActivity : BaseActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { private lateinit var binding: ActivityColorPickerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // used to fix Oreo crash if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // used to fix Oreo crash
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} }
binding = ActivityColorPickerBinding.inflate(layoutInflater)
setContentView(R.layout.activity_color_picker) setContentView(binding.root)
this.title = getString(R.string.select_a_color)
initUI() initUI()
} }
@ -49,29 +51,29 @@ class ColorPickerActivity : BaseActivity() {
* Init UI * Init UI
*/ */
private fun initUI() { private fun initUI() {
color1.setOnClickListener { manageSelectedColor(it) } binding.color1.setOnClickListener { manageSelectedColor(it) }
color2.setOnClickListener { manageSelectedColor(it) } binding.color2.setOnClickListener { manageSelectedColor(it) }
color3.setOnClickListener { manageSelectedColor(it) } binding.color3.setOnClickListener { manageSelectedColor(it) }
color4.setOnClickListener { manageSelectedColor(it) } binding.color4.setOnClickListener { manageSelectedColor(it) }
color5.setOnClickListener { manageSelectedColor(it) } binding.color5.setOnClickListener { manageSelectedColor(it) }
color6.setOnClickListener { manageSelectedColor(it) } binding.color6.setOnClickListener { manageSelectedColor(it) }
color7.setOnClickListener { manageSelectedColor(it) } binding.color7.setOnClickListener { manageSelectedColor(it) }
color8.setOnClickListener { manageSelectedColor(it) } binding.color8.setOnClickListener { manageSelectedColor(it) }
color9.setOnClickListener { manageSelectedColor(it) } binding.color9.setOnClickListener { manageSelectedColor(it) }
} }
private fun manageSelectedColor(view: View) { private fun manageSelectedColor(view: View) {
val color = when(view) { val color = when(view) {
color1 -> getColor(R.color.player_color_1) binding.color1 -> getColor(R.color.player_color_1)
color2 -> getColor(R.color.player_color_2) binding.color2 -> getColor(R.color.player_color_2)
color3 -> getColor(R.color.player_color_3) binding.color3 -> getColor(R.color.player_color_3)
color4 -> getColor(R.color.player_color_4) binding.color4 -> getColor(R.color.player_color_4)
color5 -> getColor(R.color.player_color_5) binding.color5 -> getColor(R.color.player_color_5)
color6 -> getColor(R.color.player_color_6) binding.color6 -> getColor(R.color.player_color_6)
color7 -> getColor(R.color.player_color_7) binding.color7 -> getColor(R.color.player_color_7)
color8 -> getColor(R.color.player_color_8) binding.color8 -> getColor(R.color.player_color_8)
color9 -> getColor(R.color.player_color_9) binding.color9 -> getColor(R.color.player_color_9)
else -> getColor(R.color.player_color_1) else -> getColor(R.color.player_color_1)
} }

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

@ -3,12 +3,13 @@ package net.pokeranalytics.android.ui.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import kotlinx.android.synthetic.main.activity_gdpr.* import net.pokeranalytics.android.databinding.ActivityGdprBinding
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
class GDPRActivity : BaseActivity() { class GDPRActivity : BaseActivity() {
private lateinit var binding: ActivityGdprBinding
companion object { companion object {
fun newInstance(context: Context) { fun newInstance(context: Context) {
val intent = Intent(context, GDPRActivity::class.java) val intent = Intent(context, GDPRActivity::class.java)
@ -18,7 +19,8 @@ class GDPRActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gdpr) binding = ActivityGdprBinding.inflate(layoutInflater)
setContentView(binding.root)
initUI() initUI()
} }
@ -27,10 +29,8 @@ class GDPRActivity : BaseActivity() {
* Init UI * Init UI
*/ */
private fun initUI() { private fun initUI() {
setSupportActionBar(binding.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
} }

@ -3,14 +3,15 @@ package net.pokeranalytics.android.ui.activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProvider
import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.data.LineDataSet
import kotlinx.android.synthetic.main.activity_graph.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.ActivityGraphBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.viewmodel.GraphViewModel import net.pokeranalytics.android.ui.viewmodel.GraphViewModel
import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder
@ -18,38 +19,52 @@ import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder
class GraphActivity : BaseActivity(), ViewModelHolder { class GraphActivity : BaseActivity(), ViewModelHolder {
override val model: GraphViewModel by lazy { override val model: GraphViewModel by lazy {
ViewModelProviders.of(this).get(GraphViewModel::class.java) ViewModelProvider(this).get(GraphViewModel::class.java)
} }
companion object { companion object {
const val styleKey = "style"
private var lineDataSets: List<LineDataSet>? = null private var lineDataSets: List<LineDataSet>? = null
private var barDataSets: List<BarDataSet>? = null private var barDataSets: List<BarDataSet>? = null
private var style: GraphFragment.Style = GraphFragment.Style.LINE // private var style: Graph.Style? = null
private var activityTitle: String? = null private var activityTitle: String? = null
/** /***
* Default constructor * Line graph constructor
*/ */
fun newInstance( fun newLineInstance(context: Context, lineDataSets: List<LineDataSet>, title: String? = null) {
context: Context, lineDataSets: List<LineDataSet>? = null,
barDataSets: List<BarDataSet>? = null,
style: GraphFragment.Style = GraphFragment.Style.LINE,
title: String? = null
) {
this.lineDataSets = lineDataSets this.lineDataSets = lineDataSets
// this.style = Graph.Style.LINE
this.activityTitle = title
val intent = Intent(context, GraphActivity::class.java)
intent.putExtra(styleKey, Graph.Style.LINE.ordinal)
context.startActivity(intent)
}
/***
* Bar graph constructor
*/
fun newBarInstance(context: Context, barDataSets: List<BarDataSet>, title: String? = null) {
this.barDataSets = barDataSets this.barDataSets = barDataSets
this.style = style // this.style = Graph.Style.BAR
this.activityTitle = title this.activityTitle = title
val intent = Intent(context, GraphActivity::class.java) val intent = Intent(context, GraphActivity::class.java)
intent.putExtra(styleKey, Graph.Style.BAR.ordinal)
context.startActivity(intent) context.startActivity(intent)
} }
} }
private lateinit var binding: ActivityGraphBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_graph) binding = ActivityGraphBinding.inflate(layoutInflater)
setContentView(binding.root)
initUI() initUI()
} }
@ -61,7 +76,7 @@ class GraphActivity : BaseActivity(), ViewModelHolder {
this.model.title = activityTitle this.model.title = activityTitle
this.model.title?.let { this.model.title?.let {
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = activityTitle title = activityTitle
activityTitle = null activityTitle = null
@ -82,6 +97,15 @@ class GraphActivity : BaseActivity(), ViewModelHolder {
} }
} }
val styleIndex = this.intent.getIntExtra(styleKey, -1)
val style = Graph.Style.values()[styleIndex]
// style?.let {
// this.model.style = it
// }
//
// val style = style ?: this.model.style ?: throw PAIllegalStateException("Graph style not defined")
val fragmentTransaction = supportFragmentManager.beginTransaction() val fragmentTransaction = supportFragmentManager.beginTransaction()
val graphFragment = GraphFragment.newInstance(style) val graphFragment = GraphFragment.newInstance(style)
fragmentTransaction.add(R.id.container, graphFragment) fragmentTransaction.add(R.id.container, graphFragment)

@ -7,16 +7,33 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.android.synthetic.main.activity_home.*
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.databinding.ActivityHomeBinding
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Currency import net.pokeranalytics.android.model.realm.Currency
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.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.isSameMonth
import java.util.*
enum class Tab(var identifier: Int) {
HISTORY(0),
STATS(1),
CALENDAR(2),
REPORTS(3),
SETTINGS(4)
}
class HomeActivity : BaseActivity(), NewPerformanceListener {
class HomeActivity : BaseActivity() {
companion object { companion object {
fun newInstance(context: Context, id: Int) { fun newInstance(context: Context, id: Int) {
@ -30,23 +47,29 @@ class HomeActivity : BaseActivity() {
private var homePagerAdapter: HomePagerAdapter? = null private var homePagerAdapter: HomePagerAdapter? = null
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (binding.viewPager.currentItem == Tab.REPORTS.identifier) {
this.paApplication.reportWhistleBlower?.clearNotifications()
}
when (item.itemId) { when (item.itemId) {
R.id.navigation_history -> { R.id.navigation_history -> {
displayFragment(0) displayFragment(Tab.HISTORY)
} }
R.id.navigation_stats -> { R.id.navigation_stats -> {
displayFragment(1) displayFragment(Tab.STATS)
} }
R.id.navigation_calendar -> { R.id.navigation_calendar -> {
displayFragment(2) displayFragment(Tab.CALENDAR)
} }
R.id.navigation_reports -> { R.id.navigation_reports -> {
displayFragment(3) displayFragment(Tab.REPORTS)
} }
R.id.navigation_settings -> { R.id.navigation_settings -> {
displayFragment(4) displayFragment(Tab.SETTINGS)
} }
} }
binding.navigation.getOrCreateBadge(item.itemId).isVisible = false
return@OnNavigationItemSelectedListener true return@OnNavigationItemSelectedListener true
} }
@ -54,11 +77,18 @@ class HomeActivity : BaseActivity() {
super.onResume() super.onResume()
AppGuard.requestPurchasesUpdate() AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed() this.homePagerAdapter?.activityResumed()
lookForCalendarBadge()
checkForFailedBackups()
} }
private lateinit var binding: ActivityHomeBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true) setShowWhenLocked(true)
@ -68,58 +98,18 @@ class HomeActivity : BaseActivity() {
} }
} }
setContentView(R.layout.activity_home)
observeRealmObjects() observeRealmObjects()
initUI() initUI()
checkFirstLaunch() checkFirstLaunch()
} this.paApplication.reportWhistleBlower?.addListener(this)
// override fun onNewIntent(intent: Intent?) {
// super.onNewIntent(intent)
//
// setIntent(intent)
// intent?.let {
//
// when (intent.action) {
// "android.intent.action.VIEW" -> { // import
// val data = it.data
// if (data != null) {
// this.requestImportConfirmation(data)
// } else {
// throw PAIllegalStateException("URI null on import")
// }
// }
// else -> {
// Timber.w("Intent ${intent.action} unmanaged")
// }
// }
// }
//
// }
// 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, message = R.string.unknown_import_format_popup_message)
// }
// }
// }
// }
// Import }
// private fun requestImportConfirmation(uri: Uri) { override fun onDestroy() {
// super.onDestroy()
// showAlertDialog(context = this, title = R.string.import_confirmation, showCancelButton = true, positiveAction = { this.paApplication.reportWhistleBlower?.removeListener(this)
// ImportActivity.newInstanceForResult(this, uri) }
// })
//
// }
private fun observeRealmObjects() { private fun observeRealmObjects() {
@ -127,9 +117,9 @@ class HomeActivity : BaseActivity() {
// observe currency changes // observe currency changes
this.currencies = realm.where(Currency::class.java).findAll() this.currencies = realm.where(Currency::class.java).findAll()
this.currencies.addChangeListener { t, _ -> this.currencies.addChangeListener { currencies, _ ->
realm.executeTransaction { realm.executeTransaction {
t.forEach { currencies.forEach {
it.refreshRelatedRatedValues() it.refreshRelatedRatedValues()
} }
} }
@ -141,13 +131,16 @@ class HomeActivity : BaseActivity() {
*/ */
private fun initUI() { private fun initUI() {
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener) binding.navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.selectedItemId = R.id.navigation_history binding.navigation.selectedItemId = R.id.navigation_history
homePagerAdapter = HomePagerAdapter(supportFragmentManager) homePagerAdapter = HomePagerAdapter(supportFragmentManager)
val viewPager = binding.viewPager
viewPager.offscreenPageLimit = 5 viewPager.offscreenPageLimit = 5
viewPager.enablePaging = false viewPager.enablePaging = false
viewPager.adapter = homePagerAdapter viewPager.adapter = homePagerAdapter
} }
/** /**
@ -167,8 +160,70 @@ class HomeActivity : BaseActivity() {
/** /**
* Display a new fragment * Display a new fragment
*/ */
private fun displayFragment(index: Int) { private fun displayFragment(tab: Tab) {
viewPager.setCurrentItem(index, false) binding.viewPager.setCurrentItem(tab.identifier, false)
this.homePagerAdapter?.tabSelected(tab)
}
override fun newBestPerformanceHandler() {
if (Preferences.showInAppBadges(this)) {
binding.navigation.getOrCreateBadge(R.id.navigation_reports).isVisible = true
binding.navigation.getOrCreateBadge(R.id.navigation_reports).number = 1
}
}
private fun lookForCalendarBadge() {
if (!Preferences.showInAppBadges(this)) {
return
}
val date = Preferences.lastCalendarBadgeDate(this)
val cal = Calendar.getInstance()
val lastCheck = Calendar.getInstance().apply { timeInMillis = date }
if (!cal.isSameMonth(lastCheck)) {
lookForSessionsLastMonth()
}
}
private fun lookForSessionsLastMonth() {
val cal = Calendar.getInstance()
cal.add(Calendar.MONTH, -1)
val month = QueryCondition.AnyMonthOfYear(cal.get(Calendar.MONTH))
val year = QueryCondition.AnyYear(cal.get(Calendar.YEAR))
val query = Query(month, year)
val sessions = getRealm().findAll<Session>(query)
if (sessions.isNotEmpty()) {
binding.navigation.getOrCreateBadge(R.id.navigation_calendar).isVisible = true
binding.navigation.getOrCreateBadge(R.id.navigation_calendar).number = 1
Preferences.setLastCalendarBadgeDate(this, Date().time)
}
}
private fun checkForFailedBackups() {
if (!Preferences.sessionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.SESSION, email, this)
task.start()
}
}
if (!Preferences.transactionsBackupSuccess(this)) {
Preferences.getBackupEmail(this)?.let { email ->
val task = BackupTask(DataType.TRANSACTION, email, this)
task.start()
}
}
} }
} }

@ -9,6 +9,7 @@ import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
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.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -17,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() {
@ -49,7 +49,7 @@ class ImportActivity : BaseActivity() {
intent?.data?.let { intent?.data?.let {
this.fileURI = it this.fileURI = it
} ?: run { } ?: run {
this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) as Uri this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) ?: throw PAIllegalStateException("Uri not found")
} }
setContentView(R.layout.activity_import) setContentView(R.layout.activity_import)
@ -63,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()
@ -82,7 +84,7 @@ class ImportActivity : BaseActivity() {
when (requestCode) { when (requestCode) {
RequestCode.IMPORT.value -> { RequestCode.IMPORT.value -> {
if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) { if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) {
showAlertDialog(context = this, message = R.string.unknown_import_format_popup_message, positiveAction = { showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = {
finish() finish()
}) })
} }

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
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.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
@ -20,7 +21,7 @@ class ProgressReportActivity : ReportActivity() {
*/ */
fun newInstance(context: Context, report: Report, title: String, stat: Stat? = null, displayAggregationChoices: Boolean = true) { fun newInstance(context: Context, report: Report, title: String, stat: Stat? = null, displayAggregationChoices: Boolean = true) {
val parameters = ReportParameters(report, title, stat, showAggregationChoices = displayAggregationChoices) val parameters = ReportParameters(report, ReportDisplay.PROGRESS, title, stat, showAggregationChoices = displayAggregationChoices)
ReportViewModel.defineParameters(parameters) ReportViewModel.defineParameters(parameters)
val intent = Intent(context, ProgressReportActivity::class.java) val intent = Intent(context, ProgressReportActivity::class.java)
context.startActivity(intent) context.startActivity(intent)

@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
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.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -14,6 +15,7 @@ class ReportCreationActivity : BaseActivity() {
companion object { companion object {
var options: Calculator.Options? = null var options: Calculator.Options? = null
var reportDisplay: ReportDisplay? = null
fun newInstanceForResult(fragment: Fragment, context: Context) { fun newInstanceForResult(fragment: Fragment, context: Context) {
val intent = Intent(context, ReportCreationActivity::class.java) val intent = Intent(context, ReportCreationActivity::class.java)

@ -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
@ -12,13 +13,17 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.crashlytics.android.Crashlytics
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.model.realm.Location import net.pokeranalytics.android.model.realm.Location
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.helpers.AppReviewManager
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
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
@ -36,14 +41,13 @@ abstract class BaseActivity : AppCompatActivity() {
private var permissionRequest: PermissionRequest? = null private var permissionRequest: PermissionRequest? = null
val paApplication: PokerAnalyticsApplication
get() { return this.application as PokerAnalyticsApplication }
val bottomSheetViewModel: RootBottomSheetViewModel by lazy { val bottomSheetViewModel: RootBottomSheetViewModel by lazy {
ViewModelProvider(this).get(RootBottomSheetViewModel::class.java) ViewModelProvider(this).get(RootBottomSheetViewModel::class.java)
} }
// var bottomSheetRow: RowRepresentable?
// get() { return this.bottomSheetViewModel.rowRepresentable }
// set(value) { this.bottomSheetViewModel.rowRepresentable = value }
fun setBottomSheetParameters(rowRepresentable: RowRepresentable, delegate: RowRepresentableDelegate) { fun setBottomSheetParameters(rowRepresentable: RowRepresentable, delegate: RowRepresentableDelegate) {
this.bottomSheetViewModel.rowRepresentable = rowRepresentable this.bottomSheetViewModel.rowRepresentable = rowRepresentable
this.bottomSheetViewModel.delegate = delegate this.bottomSheetViewModel.delegate = delegate
@ -53,51 +57,55 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Crashlytics.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)
Crashlytics.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() {
super.onResume() super.onResume()
Crashlytics.log("$this.localClassName onResume") CrashLogging.log("$this.localClassName onResume")
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
Crashlytics.log("$this.localClassName onPause") CrashLogging.log("$this.localClassName onPause")
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
Crashlytics.log("$this.localClassName onStop") CrashLogging.log("$this.localClassName onStop")
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Crashlytics.log("$this.localClassName onDestroy") CrashLogging.log("$this.localClassName onDestroy")
this.realm?.close() this.realm?.close()
} }
override fun onBackPressed() {
super.onBackPressed()
AppReviewManager.showReviewManager(this)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) { when (requestCode) {
PERMISSION_REQUEST_ACCESS_FINE_LOCATION -> { PERMISSION_REQUEST_ACCESS_FINE_LOCATION -> {
if (permissions.isNotEmpty() && permissions[0] == ACCESS_FINE_LOCATION if (permissions.isNotEmpty() && permissions[0] == ACCESS_FINE_LOCATION
&& grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
) {
permissionCallback?.invoke(true) permissionCallback?.invoke(true)
} else { } else {
permissionCallback?.invoke(false) permissionCallback?.invoke(false)
// permission denied, boo! Disable the
// functionality that depends on this permission.
// showMessage(getString(R.string.error));
} }
this.permissionCallback = null
} }
} }
@ -107,6 +115,7 @@ abstract class BaseActivity : AppCompatActivity() {
val allPermissionsPresent = permissions.all { request.permissions.contains(it) } val allPermissionsPresent = permissions.all { request.permissions.contains(it) }
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
request.callback.invoke(allPermissionsPresent && allGranted) request.callback.invoke(allPermissionsPresent && allGranted)
this.permissionRequest = null
} }
} }
@ -125,6 +134,27 @@ 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
*/ */
@ -145,6 +175,14 @@ abstract class BaseActivity : AppCompatActivity() {
return ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED return ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
} }
/**
* Ask for app permission
*/
fun askForPermission(permissions: Array<String>, requestCode: Int, permissionCallback: ((granted: Boolean) -> Unit)) {
this.permissionRequest = PermissionRequest(permissions, permissionCallback, requestCode)
ActivityCompat.requestPermissions(this, permissions, requestCode)
}
/** /**
* Ask for location permission * Ask for location permission
*/ */
@ -155,24 +193,6 @@ abstract class BaseActivity : AppCompatActivity() {
) )
} }
/**
* Ask for places request
*/
// fun askForPlacesRequest(callback: ((success: Boolean, places: ArrayList<PlaceLikelihood>) -> Unit)?) {
// // Call findCurrentPlace and handle the response (first check that the user has granted permission).
// if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
// LocationManager(this).askForPlacesRequest(callback)
// } else {
// askForLocationPermission { granted ->
// if (granted) {
// LocationManager(this).askForPlacesRequest(callback)
// } else {
// callback?.invoke(false, ArrayList())
// }
// }
// }
// }
/** /**
* Find the nearest location from the user * Find the nearest location from the user
*/ */
@ -194,14 +214,6 @@ abstract class BaseActivity : AppCompatActivity() {
} }
} }
/**
* Ask for app permission
*/
fun askForPermission(permissions: Array<String>, requestCode: Int, permissionCallback: ((granted: Boolean) -> Unit)) {
this.permissionRequest = PermissionRequest(permissions, permissionCallback, requestCode)
ActivityCompat.requestPermissions(this, permissions, requestCode)
}
/** /**
* Find the current location * Find the current location
*/ */

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

@ -4,6 +4,7 @@ import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -136,6 +137,7 @@ open class MediaActivity : BaseActivity() {
selectedChoice = -1 selectedChoice = -1
} }
// Build.VERSION.SDK_INT < Build.VERSION_CODES.R &&
/** /**
* Open the Camera Intent * Open the Camera Intent
@ -147,35 +149,40 @@ open class MediaActivity : BaseActivity() {
this.mCurrentPhotoPath = null this.mCurrentPhotoPath = null
this.multiplePictures = multiplePictures this.multiplePictures = multiplePictures
// Test if we have the permission askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 200) { granted ->
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || granted) {
selectedChoice = SELECTED_CHOICE_TAKE_PICTURE
askForStoragePermission()
return
}
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(packageManager) != null) { // Create the File where the photo should go
// Create the File where the photo should go try {
try { tempFile = ImageUtils.createImageFile(this)
tempFile = ImageUtils.createImageFile(this) mCurrentPhotoPath = "file:" + tempFile?.absolutePath
mCurrentPhotoPath = "file:" + tempFile?.absolutePath } catch (ex: IOException) {
} catch (ex: IOException) { // Error occurred while creating the File
// Error occurred while creating the File }
}
// Continue only if the File was successfully created
if (tempFile != null) {
Timber.d("tempFile: ${tempFile?.absolutePath}")
val photoURI = FileProvider.getUriForFile(
this,
applicationContext.packageName + ".fileprovider", tempFile!!
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE)
}
// Continue only if the File was successfully created
if (tempFile != null) {
Timber.d("tempFile: ${tempFile?.absolutePath}")
val photoURI = FileProvider.getUriForFile(
this,
applicationContext.packageName + ".fileprovider", tempFile!!
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE)
} }
} }
// // Test if we have the permission
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// selectedChoice = SELECTED_CHOICE_TAKE_PICTURE
// askForStoragePermission()
// return
// }
} }
@ -188,20 +195,32 @@ open class MediaActivity : BaseActivity() {
this.multiplePictures = multiplePictures this.multiplePictures = multiplePictures
// Test if we have the permission askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 201) { granted ->
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || granted) {
selectedChoice = SELECTED_CHOICE_SELECT_PICTURE val galleryIntent = Intent()
askForStoragePermission() galleryIntent.type = "image/*"
return galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures)
galleryIntent.action = Intent.ACTION_GET_CONTENT
startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
}
} }
this.multiplePictures = multiplePictures
val galleryIntent = Intent() // Test if we have the permission
galleryIntent.type = "image/*" // if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures) // selectedChoice = SELECTED_CHOICE_SELECT_PICTURE
galleryIntent.action = Intent.ACTION_GET_CONTENT // askForStoragePermission()
startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE) // return
// }
//
// this.multiplePictures = multiplePictures
//
// val galleryIntent = Intent()
// galleryIntent.type = "image/*"
// galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures)
// galleryIntent.action = Intent.ACTION_GET_CONTENT
// startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
} }
/** /**
@ -215,43 +234,6 @@ open class MediaActivity : BaseActivity() {
} }
} }
// /**
// * Ask for the acmera permission
// */
// private fun askForCameraPermission() {
// // Here, thisActivity is the current activity
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),
// PERMISSION_REQUEST_CAMERA)
// }
// }
// /**
// * Ask for camera and storage permission
// */
// private fun askForCameraAndStoragePermissions() {
//
// val permissions = ArrayList<String>()
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// permissions.add(Manifest.permission.CAMERA)
// }
//
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
// }
//
// if (permissions.size > 0) {
// ActivityCompat.requestPermissions(this, permissions.toArray(arrayOfNulls<String>(permissions.size)), PERMISSION_REQUEST_CAMERA)
// }
// }
//
// /**
// * Called when a bitmap is return
// *
// * @param bitmap the bitmap returned
// */
// open fun getBitmapImage(file: File?, bitmap: Bitmap?) {}
/** /**
* Called when the user is adding new photos * Called when the user is adding new photos

@ -4,18 +4,19 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProvider
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
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.ui.viewmodel.ReportViewModel import net.pokeranalytics.android.ui.viewmodel.ReportViewModel
import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder import net.pokeranalytics.android.ui.viewmodel.ViewModelHolder
class ReportParameters(var report: Report, var title: String, var stat: Stat? = null, var showAggregationChoices: Boolean = true) class ReportParameters(var report: Report, var reportDisplay: ReportDisplay, var title: String, var stat: Stat? = null, var showAggregationChoices: Boolean = true)
abstract class ReportActivity : BaseActivity(), ViewModelHolder { abstract class ReportActivity : BaseActivity(), ViewModelHolder {
override val model: ReportViewModel by lazy { override val model: ReportViewModel by lazy {
ViewModelProviders.of(this).get(ReportViewModel::class.java) ViewModelProvider(this).get(ReportViewModel::class.java)
} }
companion object { companion object {
@ -23,19 +24,19 @@ abstract class ReportActivity : BaseActivity(), ViewModelHolder {
/** /**
* Default constructor * Default constructor
*/ */
fun newInstance(context: Context, report: Report, reportTitle: String, stat: Stat? = null) { fun newInstance(context: Context, report: Report, reportDisplay: ReportDisplay, reportTitle: String, stat: Stat? = null) {
val options = report.options // val options = report.options
val parameters = ReportParameters(report, reportTitle, stat) val parameters = ReportParameters(report, reportDisplay, reportTitle, stat)
ReportViewModel.defineParameters(parameters) ReportViewModel.defineParameters(parameters)
val intent = Intent(context, options.display.activityClass) val intent = Intent(context, reportDisplay.activityClass)
context.startActivity(intent) context.startActivity(intent)
} }
fun newInstanceForResult(fragment: Fragment, report: Report, reportTitle: String, stat: Stat? = null) { fun newInstanceForResult(fragment: Fragment, report: Report, reportDisplay: ReportDisplay, reportTitle: String, stat: Stat? = null) {
val options = report.options // val options = report.options
val parameters = ReportParameters(report, reportTitle, stat) val parameters = ReportParameters(report, reportDisplay, reportTitle, stat)
ReportViewModel.defineParameters(parameters) ReportViewModel.defineParameters(parameters)
val intent = Intent(fragment.requireContext(), options.display.activityClass) val intent = Intent(fragment.requireContext(), reportDisplay.activityClass)
fragment.startActivityForResult(intent, RequestCode.DEFAULT.value) fragment.startActivityForResult(intent, RequestCode.DEFAULT.value)
} }
@ -50,6 +51,7 @@ abstract class ReportActivity : BaseActivity(), ViewModelHolder {
ReportViewModel.parameters?.let { ReportViewModel.parameters?.let {
this.model.report = it.report this.model.report = it.report
this.model.reportDisplay = it.reportDisplay
this.model.title = it.title this.model.title = it.title
val stat = it.stat ?: it.report.options.stats.first() val stat = it.stat ?: it.report.options.stats.first()
this.model.stat = stat this.model.stat = stat

@ -6,10 +6,11 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.modules.feed.FeedFragment
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
/** /**
@ -21,10 +22,10 @@ class ComparisonChartPagerAdapter(val context: Context, fragmentManager: Fragmen
override fun getItem(position: Int): BaseFragment { override fun getItem(position: Int): BaseFragment {
return when (position) { return when (position) {
0 -> GraphFragment.newInstance(GraphFragment.Style.BAR) 0 -> GraphFragment.newInstance(Graph.Style.BAR)
1 -> GraphFragment.newInstance(GraphFragment.Style.MULTILINE) 1 -> GraphFragment.newInstance(Graph.Style.MULTILINE)
2 -> CalendarFragment.newInstance() 2 -> CalendarFragment.newInstance()
else -> FeedFragment.newInstance() else -> throw PAIllegalStateException("There should not be more than $count items, position = $position")
} }
} }

@ -2,13 +2,17 @@ package net.pokeranalytics.android.ui.adapter
import android.util.SparseArray import android.util.SparseArray
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.util.forEach
import androidx.core.util.size
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.Tab
import net.pokeranalytics.android.ui.fragment.ReportsFragment import net.pokeranalytics.android.ui.fragment.ReportsFragment
import net.pokeranalytics.android.ui.fragment.SettingsFragment import net.pokeranalytics.android.ui.fragment.SettingsFragment
import net.pokeranalytics.android.ui.fragment.StatisticsFragment import net.pokeranalytics.android.ui.fragment.StatisticsFragment
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.modules.calendar.CalendarFragment
import net.pokeranalytics.android.ui.modules.feed.FeedFragment import net.pokeranalytics.android.ui.modules.feed.FeedFragment
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -16,23 +20,23 @@ import java.lang.ref.WeakReference
* Home Adapter * Home Adapter
*/ */
class HomePagerAdapter(fragmentManager: FragmentManager) : class HomePagerAdapter(fragmentManager: FragmentManager) :
FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private var weakReferences = SparseArray<WeakReference<BaseFragment>>() private var weakReferences = SparseArray<WeakReference<BaseFragment>>()
override fun getItem(position: Int): BaseFragment { override fun getItem(position: Int): BaseFragment {
return when (position) { return when (position) {
0 -> FeedFragment.newInstance() Tab.HISTORY.identifier -> FeedFragment.newInstance()
1 -> StatisticsFragment.newInstance() Tab.STATS.identifier -> StatisticsFragment.newInstance()
2 -> CalendarFragment.newInstance() Tab.CALENDAR.identifier -> CalendarFragment.newInstance()
3 -> ReportsFragment.newInstance() Tab.REPORTS.identifier -> ReportsFragment.newInstance()
4 -> SettingsFragment.newInstance() Tab.SETTINGS.identifier -> SettingsFragment.newInstance()
else -> FeedFragment.newInstance() else -> throw PAIllegalStateException("Should not happen, position = $position")
} }
} }
override fun getCount(): Int { override fun getCount(): Int {
return 5 return Tab.values().size
} }
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
@ -48,30 +52,27 @@ class HomePagerAdapter(fragmentManager: FragmentManager) :
override fun getItemPosition(obj: Any): Int { override fun getItemPosition(obj: Any): Int {
return when (obj) { return when (obj) {
FeedFragment::class.java -> 0 is FeedFragment -> Tab.HISTORY.identifier
StatisticsFragment::class.java -> 1 is StatisticsFragment -> Tab.STATS.identifier
CalendarFragment::class.java -> 2 is CalendarFragment -> Tab.CALENDAR.identifier
ReportsFragment::class.java -> 3 is ReportsFragment -> Tab.REPORTS.identifier
SettingsFragment::class.java -> 4 is SettingsFragment -> Tab.SETTINGS.identifier
else -> -1 else -> throw PAIllegalStateException("Should not happen for object $obj")
} }
} }
fun activityResumed() { fun activityResumed() {
val ref = this.weakReferences.get(0)
ref?.get()?.let { this.weakReferences.forEach { _, value ->
(it as FeedFragment).activityResumed() value.get()?.activityResumed()
} }
} }
// /** fun tabSelected(tab: Tab) {
// * Return the fragment at the position key if (this.weakReferences.size >= 5) {
// */ this.weakReferences.get(tab.identifier).get()?.selectedTab()
// fun getFragment(key: Int): BaseFragment? { }
// if (this.weakReferences.get(key) != null) { }
// return this.weakReferences.get(key).get()
// }
// return null
// }
} }

@ -8,6 +8,7 @@ import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.viewmodel.ReportHolder import net.pokeranalytics.android.ui.viewmodel.ReportHolder
/** /**
@ -19,8 +20,8 @@ class ReportPagerAdapter(private val context: Context,
override fun getItem(position: Int): BaseFragment { override fun getItem(position: Int): BaseFragment {
return when (position) { return when (position) {
0 -> GraphFragment.newInstance(style = GraphFragment.Style.BAR) 0 -> GraphFragment.newInstance(Graph.Style.BAR)
1 -> GraphFragment.newInstance(style = GraphFragment.Style.MULTILINE) 1 -> GraphFragment.newInstance(Graph.Style.MULTILINE)
2 -> { 2 -> {
val report = this.reportHolder.report val report = this.reportHolder.report
ComposableTableReportFragment.newInstance(report) ComposableTableReportFragment.newInstance(report)
@ -33,7 +34,7 @@ class ReportPagerAdapter(private val context: Context,
return 3 return 3
} }
override fun getPageTitle(position: Int): CharSequence? { override fun getPageTitle(position: Int): CharSequence {
return when(position) { return when(position) {
0 -> context.getString(R.string.bars) 0 -> context.getString(R.string.bars)
1 -> context.getString(R.string.lines) 1 -> context.getString(R.string.lines)

@ -1,6 +1,5 @@
package net.pokeranalytics.android.ui.adapter package net.pokeranalytics.android.ui.adapter
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -65,4 +64,8 @@ class RowRepresentableAdapter(
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
fun changeDataSource(dataSource: RowRepresentableDataSource) {
this.dataSource = dataSource
}
} }

@ -9,14 +9,12 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.text.SpannableStringBuilder
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ImageView import android.widget.*
import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -36,177 +34,257 @@ import java.io.File
// Sizes // Sizes
val Int.dp: Int val Int.dp: Int
get() = (this / Resources.getSystem().displayMetrics.density).toInt() get() = (this / Resources.getSystem().displayMetrics.density).toInt()
val Int.px: Int val Int.px: Int
get() = (this * Resources.getSystem().displayMetrics.density).toInt() get() = (this * Resources.getSystem().displayMetrics.density).toInt()
val Float.dp: Float val Float.dp: Float
get() = (this / Resources.getSystem().displayMetrics.density) get() = (this / Resources.getSystem().displayMetrics.density)
val Float.px: Float val Float.px: Float
get() = (this * Resources.getSystem().displayMetrics.density) get() = (this * Resources.getSystem().displayMetrics.density)
// Toast // Toast
fun Activity.toast(message: String) { fun Activity.toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
fun Fragment.toast(message: String) { fun Fragment.toast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
} }
// Open Play Store for rating // Open Play Store for rating
fun Activity.openPlayStorePage() { fun Activity.openPlayStorePage() {
val uri = Uri.parse("market://details?id=$packageName") val uri = Uri.parse("market://details?id=$packageName")
val goToMarket = Intent(Intent.ACTION_VIEW, uri) val goToMarket = Intent(Intent.ACTION_VIEW, uri)
goToMarket.addFlags( goToMarket.addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK Intent.FLAG_ACTIVITY_MULTIPLE_TASK
) )
try { try {
startActivity(goToMarket) startActivity(goToMarket)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
startActivity( startActivity(
Intent( Intent(
Intent.ACTION_VIEW, Uri.parse( Intent.ACTION_VIEW, Uri.parse(
"http://play.google.com/store/apps/details?id=$packageName" "http://play.google.com/store/apps/details?id=$packageName"
) )
) )
) )
} }
} }
// Open email for "Contact us" // Open email for "Contact us"
fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null) { fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null) {
val info = val info =
"v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}"
val emailIntent = Intent(Intent.ACTION_SEND) val emailIntent = Intent(Intent.ACTION_SEND)
filePath?.let { filePath?.let {
val databaseFile = File(it) val databaseFile = File(it)
val contentUri = FileProvider.getUriForFile(this, "net.pokeranalytics.android.fileprovider", databaseFile) val contentUri = FileProvider.getUriForFile(
if (contentUri != null) { this,
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) "net.pokeranalytics.android.fileprovider",
emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri)) databaseFile
emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri) )
} if (contentUri != null) {
} emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri))
emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
}
}
emailIntent.type = "message/rfc822" emailIntent.type = "message/rfc822"
emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes)) emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes))
emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info") emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info")
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value)) emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value))
startActivity(Intent.createChooser(emailIntent, getString(R.string.contact))) startActivity(Intent.createChooser(emailIntent, getString(R.string.contact)))
} }
// Open custom tab // Open custom tab
fun Context.openUrl(url: String) { fun Context.openUrl(url: String) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
ContextCompat.startActivity(this, browserIntent, null) ContextCompat.startActivity(this, browserIntent, null)
}
// Open custom tab
fun Context.areYouSure(
title: Int? = null,
message: Int? = null,
positiveTitle: Int? = null,
proceed: () -> Unit
) {
val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this)
val messageResource = message ?: R.string.are_you_sure_you_want_to_do_this
builder.setMessage(messageResource)
title?.let { builder.setTitle(it) }
val positiveButtonTitle = positiveTitle ?: R.string.yes
builder.setPositiveButton(positiveButtonTitle) { _, _ ->
proceed()
}
builder.setNegativeButton(R.string.cancel) { _, _ ->
// nothing
}
builder.create().show()
} }
// Display Alert Dialog // Display Alert Dialog
fun Activity.showAlertDialog(title: Int? = null, message: Int? = null) { fun Activity.showAlertDialog(title: Int? = null, message: Int? = null) {
showAlertDialog(this, title, message) showAlertDialog(this, title, message)
} }
fun Fragment.showAlertDialog(title: Int? = null, message: Int? = null) { fun Fragment.showAlertDialog(
context?.let { title: Int? = null,
showAlertDialog(it, title, message) messageResId: Int? = null,
} message: String? = null
) {
context?.let {
showAlertDialog(it, title, messageResId, message)
}
}
fun showEditTextAlertDialog(
context: Context, inputType: Int, title: Int? = null, messageResId: Int? = null, message: String? = null,
editTextText: String? = null, positiveAction: ((String) -> Unit)? = null
) {
val builder = AlertDialog.Builder(context)
title?.let {
builder.setTitle(title)
}
messageResId?.let {
builder.setMessage(messageResId)
}
message?.let {
builder.setMessage(it)
}
val layout = LinearLayout(context)
layout.orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
)
params.setMargins(20, 0, 30, 0)
val editText = EditText(context)
editText.inputType = inputType
editTextText?.let {
editText.text = SpannableStringBuilder(it)
}
editText.setTextColor(ContextCompat.getColor(context, R.color.white))
layout.addView(editText, params)
builder.setView(layout)
// builder.setView(editText)
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
positiveAction?.invoke(editText.text.toString())
}
builder.show()
} }
/** /**
* Create and show an alert dialog * Create and show an alert dialog
*/ */
fun showAlertDialog( fun showAlertDialog(
context: Context, title: Int? = null, message: Int? = null, cancelButtonTitle: Int? = null, showCancelButton: Boolean = false, context: Context, title: Int? = null, messageResId: Int? = null, message: String? = null,
positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null cancelButtonTitle: Int? = null, showCancelButton: Boolean = false,
positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null
) { ) {
val builder = AlertDialog.Builder(context) val builder = AlertDialog.Builder(context)
title?.let { title?.let {
builder.setTitle(title) builder.setTitle(title)
} }
message?.let { messageResId?.let {
builder.setMessage(message) builder.setMessage(messageResId)
} }
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> message?.let {
positiveAction?.invoke() builder.setMessage(it)
} }
if (cancelButtonTitle != null) { builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
builder.setNegativeButton(cancelButtonTitle) { _, _ -> positiveAction?.invoke()
negativeAction?.invoke() }
}
} else if (showCancelButton) { if (cancelButtonTitle != null) {
builder.setNegativeButton(R.string.cancel) { _, _ -> builder.setNegativeButton(cancelButtonTitle) { _, _ ->
negativeAction?.invoke() negativeAction?.invoke()
} }
} } else if (showCancelButton) {
builder.show() builder.setNegativeButton(R.string.cancel) { _, _ ->
} negativeAction?.invoke()
}
fun AppCompatTextView.setTextFormat(textFormat: TextFormat, context: Context) { }
this.setTextColor(textFormat.getColor(context)) builder.show()
this.text = textFormat.text }
fun TextView.setTextFormat(textFormat: TextFormat, context: Context) {
this.setTextColor(textFormat.getColor(context))
this.text = textFormat.text
} }
fun View.hideWithAnimation() { fun View.hideWithAnimation() {
isVisible = true isVisible = true
animate().cancel() animate().cancel()
animate().alpha(0f).withEndAction { isVisible = false }.start() animate().alpha(0f).withEndAction { isVisible = false }.start()
} }
fun View.showWithAnimation() { fun View.showWithAnimation() {
isVisible = true isVisible = true
animate().cancel() animate().cancel()
animate().alpha(1f).start() animate().alpha(1f).start()
} }
fun View.addCircleRipple() = with(TypedValue()) { fun View.addCircleRipple() = with(TypedValue()) {
context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true) context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true)
setBackgroundResource(resourceId) setBackgroundResource(resourceId)
} }
fun SearchView.removeMargins() { fun SearchView.removeMargins() {
val searchEditFrame = findViewById<LinearLayout?>(R.id.search_edit_frame) val searchEditFrame = findViewById<LinearLayout?>(R.id.search_edit_frame)
val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams? val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams?
layoutParams?.leftMargin = 0 layoutParams?.leftMargin = 0
layoutParams?.rightMargin = 0 layoutParams?.rightMargin = 0
searchEditFrame?.layoutParams = layoutParams searchEditFrame?.layoutParams = layoutParams
} }
fun View.toByteArray() : ByteArray { fun View.toByteArray(): ByteArray {
return this.convertToBitmap().toByteArray() return this.convertToBitmap().toByteArray()
} }
fun View.convertToBitmap(): Bitmap { fun View.convertToBitmap(): Bitmap {
val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(b) val canvas = Canvas(b)
val background = this.background val background = this.background
this.background?.let { this.background?.let {
background.draw(canvas) background.draw(canvas)
} ?: run { } ?: run {
canvas.drawColor(Color.WHITE) canvas.drawColor(Color.WHITE)
} }
this.draw(canvas) this.draw(canvas)
return b return b
} }
fun ImageView.toByteArray() : ByteArray { fun ImageView.toByteArray(): ByteArray {
return this.drawable.toBitmap().toByteArray() return this.drawable.toBitmap().toByteArray()
} }
fun Bitmap.toByteArray() : ByteArray { fun Bitmap.toByteArray(): ByteArray {
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
this.compress(Bitmap.CompressFormat.PNG, 100, baos) this.compress(Bitmap.CompressFormat.PNG, 100, baos)
return baos.toByteArray() return baos.toByteArray()
} }
fun Context.hideKeyboard(v: View) { fun Context.hideKeyboard(v: View) {
val imm = v.context.getSystemService(InputMethodManager::class.java) val imm = v.context.getSystemService(InputMethodManager::class.java)
imm?.hideSoftInputFromWindow(v.windowToken, 0) imm?.hideSoftInputFromWindow(v.windowToken, 0)
} }
//fun Context.showKeyboard(view: View) { //fun Context.showKeyboard(view: View) {

@ -2,8 +2,8 @@ package net.pokeranalytics.android.ui.fragment
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import kotlinx.android.synthetic.main.fragment_comparison_chart.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentComparisonChartBinding
import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity
import net.pokeranalytics.android.ui.activity.SettingsActivity import net.pokeranalytics.android.ui.activity.SettingsActivity
import net.pokeranalytics.android.ui.adapter.ComparisonChartPagerAdapter import net.pokeranalytics.android.ui.adapter.ComparisonChartPagerAdapter
@ -12,7 +12,7 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.toast import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.MoreTabRow import net.pokeranalytics.android.ui.view.rows.MoreTabRow
class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
@ -39,11 +39,19 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource
private lateinit var viewPagerAdapter: ComparisonChartPagerAdapter private lateinit var viewPagerAdapter: ComparisonChartPagerAdapter
private var comparisonChartMenu: Menu? = null private var comparisonChartMenu: Menu? = null
private var _binding: FragmentComparisonChartBinding? = null
private val binding get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
// Life Cycle // Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_comparison_chart, container, false) _binding = FragmentComparisonChartBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -67,7 +75,7 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource
} }
// Rows // Rows
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable> {
return rowRepresentation return rowRepresentation
} }
@ -97,9 +105,9 @@ class ComparisonChartFragment : BaseFragment(), StaticRowRepresentableDataSource
parentActivity?.let { parentActivity?.let {
viewPagerAdapter = ComparisonChartPagerAdapter(requireContext(), it.supportFragmentManager) viewPagerAdapter = ComparisonChartPagerAdapter(requireContext(), it.supportFragmentManager)
viewPager.adapter = viewPagerAdapter binding.viewPager.adapter = viewPagerAdapter
viewPager.offscreenPageLimit = 2 binding.viewPager.offscreenPageLimit = 2
tabs.setupWithViewPager(viewPager) binding.tabs.setupWithViewPager(binding.viewPager)
} }
} }

@ -8,15 +8,15 @@ 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 androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_data_list.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentCurrenciesBinding
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.BaseFragment
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.rowrepresentable.SeparatorRow import net.pokeranalytics.android.ui.view.rows.SeparatorRow
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import java.util.* import java.util.*
@ -39,42 +39,71 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
private val mostUsedCurrencies = this.mostUsedCurrencyCodes.map { code -> private val mostUsedCurrencies = this.mostUsedCurrencyCodes.map { code ->
CurrencyRow( CurrencyRow(
this.systemCurrencies.filter { this.systemCurrencies.first {
it.currencyCode == code it.currencyCode == code
}.first() }
) )
} }
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 currencySymbole: String = currency.getSymbol(Locale.getDefault()) var currencySymbol: String = this.currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})" var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
} }
private var _binding: FragmentCurrenciesBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // Life Cycle
return inflater.inflate(R.layout.fragment_currencies, container, false)
} override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCurrenciesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -85,8 +114,8 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
// StaticRowRepresentableDataSource // StaticRowRepresentableDataSource
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable> {
return CurrenciesFragment.rowRepresentation return rowRepresentation
} }
@ -120,7 +149,7 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
val viewManager = LinearLayoutManager(requireContext()) val viewManager = LinearLayoutManager(requireContext())
val dataListAdapter = RowRepresentableAdapter(this, this) val dataListAdapter = RowRepresentableAdapter(this, this)
recyclerView.apply { binding.recyclerView.apply {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = viewManager layoutManager = viewManager
adapter = dataListAdapter adapter = dataListAdapter

@ -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
@ -13,12 +15,12 @@ import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet
import com.github.mikephil.charting.listener.OnChartValueSelectedListener import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import kotlinx.android.synthetic.main.fragment_graph.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.databinding.FragmentGraphBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.ObjectIdentifier import net.pokeranalytics.android.model.interfaces.ObjectIdentifier
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.graph.setStyle import net.pokeranalytics.android.ui.graph.setStyle
import net.pokeranalytics.android.ui.view.LegendView import net.pokeranalytics.android.ui.view.LegendView
@ -31,58 +33,80 @@ import timber.log.Timber
class GraphFragment : RealmFragment(), OnChartValueSelectedListener { class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
private lateinit var graphDataProvider: GraphDataProvider
private val stat: Stat
get() {
return this.graphDataProvider.stat
}
enum class Style {
LINE,
BAR,
MULTILINE,
}
companion object { companion object {
/** fun newInstance(defaultStyle: Graph.Style? = null): GraphFragment {
* Create new instance
*/
fun newInstance(style: Style = Style.LINE): GraphFragment {
val fragment = GraphFragment() val fragment = GraphFragment()
// fragment.lineDataSetList = lineDataSets
// fragment.barDataSetList = barDataSets
val bundle = Bundle() val bundle = Bundle()
bundle.putSerializable(BundleKey.STYLE.value, style.ordinal) defaultStyle?.let {
bundle.putInt(BundleKey.STYLE.value, defaultStyle.ordinal)
}
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment
} }
} }
private var style: Style = Style.LINE /***
* The view model
*/
private lateinit var graphDataProvider: GraphDataProvider
private val stat: Stat
get() {
return this.graphDataProvider.stat
}
/***
* Uses forced style first, or the style set
*/
private val style: Graph.Style
get() {
val style = this.graphDataProvider.style
if (style != null) {
return style
}
this.arguments?.let { bundle ->
if (bundle.containsKey(BundleKey.STYLE.value)) {
val styleIndex = bundle.getInt(BundleKey.STYLE.value)
return Graph.Style.values()[styleIndex]
} else {
return Graph.Style.LINE
// throw PAIllegalStateException("Style not defined for $this")
}
} ?: throw PAIllegalStateException("Style and bundle not defined for $this")
}
private lateinit var legendView: LegendView private lateinit var legendView: LegendView
private var chartView: BarLineChartBase<*>? = null private var chartView: BarLineChartBase<*>? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { private var _binding: FragmentGraphBinding? = null
super.onCreateView(inflater, container, savedInstanceState) private val binding get() = _binding!!
return inflater.inflate(R.layout.fragment_graph, container, false)
} // Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = FragmentGraphBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initData() initData()
initUI() initUI()
loadGraph() loadGraph()
} }
private fun initData() { private fun initData() {
val styleOrdinal = this.arguments?.getInt(BundleKey.STYLE.value) ?: throw PAIllegalStateException("Missing style key in bundle")
this.style = Style.values()[styleOrdinal]
this.graphDataProvider = (requireActivity() as ViewModelHolder).model as GraphDataProvider this.graphDataProvider = (requireActivity() as ViewModelHolder).model as GraphDataProvider
} }
@ -92,15 +116,15 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
private fun initUI() { private fun initUI() {
this.legendView = when (this.style) { this.legendView = when (this.style) {
Style.MULTILINE -> MultiLineLegendView(requireContext()) Graph.Style.MULTILINE -> MultiLineLegendView(requireContext())
else -> LegendView(requireContext()) else -> LegendView(requireContext())
} }
this.legendContainer.addView(this.legendView) this.binding.legendContainer.addView(this.legendView)
} }
fun reload(style: Style) { fun reload() {
this.style = style Timber.d("isAdded = $isAdded, isDetached = $isDetached")
if (isAdded && !isDetached) { if (isAdded && !isDetached) {
loadGraph() loadGraph()
} }
@ -111,14 +135,14 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
*/ */
private fun loadGraph() { private fun loadGraph() {
this.chartContainer.removeAllViews() this.binding.chartContainer.removeAllViews()
this.chartView = when (this.style) { this.chartView = when (this.style) {
Style.LINE, Style.MULTILINE -> { Graph.Style.LINE, Graph.Style.MULTILINE -> {
val dataSets = when (this.style) { val dataSets = when (this.style) {
Style.LINE -> listOf(this.graphDataProvider.lineDataSet(requireContext())) Graph.Style.LINE -> listOf(this.graphDataProvider.lineDataSet(requireContext()))
Style.MULTILINE -> this.graphDataProvider.multiLineDataSet(requireContext()) Graph.Style.MULTILINE -> this.graphDataProvider.multiLineDataSet(requireContext())
else -> throw PAIllegalStateException("Cannot happen") else -> throw PAIllegalStateException("Cannot happen")
} }
@ -138,7 +162,7 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
lineChart lineChart
} }
Style.BAR -> { Graph.Style.BAR -> {
val dataSets = listOf(this.graphDataProvider.barDataSet(requireContext())) val dataSets = listOf(this.graphDataProvider.barDataSet(requireContext()))
@ -167,7 +191,7 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
} }
} }
this.chartContainer.addView(this.chartView) this.binding.chartContainer.addView(this.chartView)
this.chartView?.setStyle(false, this.graphDataProvider.axisFormatting, requireContext()) this.chartView?.setStyle(false, this.graphDataProvider.axisFormatting, requireContext())
@ -201,7 +225,7 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
val groupName = dataSet?.label ?: "" val groupName = dataSet?.label ?: ""
val color = dataSet?.color val color = dataSet?.color
val legendValue = statEntry.legendValues(stat, entry, this.style, groupName, requireContext()) val legendValue = statEntry.legendValues(stat, entry.y.toDouble(), this.style, groupName, requireContext())
this.legendView.setItemData(legendValue, color) this.legendView.setItemData(legendValue, color)
} else { } else {

@ -1,36 +1,53 @@
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
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_import.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope 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.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
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext
class ImportFragment : RealmFragment(), ImportDelegate { class ImportFragment : RealmFragment(), ImportDelegate {
// val coroutineContext: CoroutineContext
// get() = Dispatchers.Main
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
fun setData(path: String) { private var _binding: FragmentImportBinding? = null
private val binding get() = _binding!!
private val exceptions: MutableList<Exception> = mutableListOf()
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = FragmentImportBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun setData(path: String) {
this.filePath = path this.filePath = path
} }
@ -38,9 +55,8 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.inputStream = inputStream this.inputStream = inputStream
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { fun setData(uri: Uri) {
super.onCreateView(inflater, container, savedInstanceState) this.uri = uri
return inflater.inflate(R.layout.fragment_import, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -53,15 +69,20 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private fun initUI() { private fun initUI() {
this.imported.text = requireContext().getString(R.string.imported) val imported = binding.imported
this.total.text = requireContext().getString(R.string.total) val total = binding.total
val save = binding.save
val cancel = binding.cancel
imported.text = requireContext().getString(R.string.imported)
total.text = requireContext().getString(R.string.total)
this.save.isEnabled = false save.isEnabled = false
this.save.setOnClickListener { save.setOnClickListener {
this.end() this.end()
} }
this.cancel.setOnClickListener { cancel.setOnClickListener {
this.cancel() this.cancel()
this.end() this.end()
} }
@ -70,34 +91,33 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private fun startImport() { private fun startImport() {
// var shouldDismissActivity = false this.parentActivity?.paApplication?.reportWhistleBlower?.pause()
this.importer = CSVImporter(inputStream) this.importer = CSVImporter(uri, requireContext())
this.importer.delegate = this this.importer.delegate = this
var exception: Exception? = null CoroutineScope(coroutineContext).launch {
GlobalScope.launch(coroutineContext) { val coroutine = GlobalScope.async {
val test = GlobalScope.async {
val s = Date() val s = Date()
Timber.d(">>> Start Import...") Timber.d(">>> Start Import...")
try { try {
importer.start() importer.start()
} catch (e: Exception) { } catch (e: ImportException) {
exception = e 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
Timber.d(">>> Import ended in $duration seconds") Timber.d(">>> Import ended in $duration seconds")
} }
test.await() coroutine.await()
val exceptionMessage = exception?.message val exceptionMessage = exceptions.firstOrNull()?.message
if (exceptionMessage != null && view != null) { if (exceptionMessage != null && view != null) {
val message = exceptionMessage + ". " + requireContext().getString(R.string.import_error) val message = "${exceptions.size} " + requireContext().getString(R.string.errors) + ":\n" + exceptionMessage + ".\n" + requireContext().getString(R.string.import_error)
val snackBar = Snackbar.make(view!!, message, Snackbar.LENGTH_INDEFINITE) val snackBar = Snackbar.make(view!!, message, Snackbar.LENGTH_INDEFINITE)
snackBar.setAction(R.string.ok) { snackBar.setAction(R.string.ok) {
snackBar.dismiss() snackBar.dismiss()
@ -107,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()
} }
@ -127,22 +138,25 @@ class ImportFragment : RealmFragment(), ImportDelegate {
} }
private fun importDidFinish() { private fun importDidFinish() {
binding.save.isEnabled = true
this.save.isEnabled = true
} }
private fun end() { private fun end() {
this.parentActivity?.paApplication?.reportWhistleBlower?.resume()
activity?.finish() activity?.finish()
} }
val numberFormatter = NumberFormat.getNumberInstance() private val numberFormatter = NumberFormat.getNumberInstance()
// ImportDelegate // ImportDelegate
override fun parsingCountUpdate(importedCount: Int, totalCount: Int) { override fun parsingCountUpdate(importedCount: Int, totalCount: Int) {
this.counter.text = this.numberFormatter.format(importedCount) binding.counter.text = this.numberFormatter.format(importedCount)
this.totalCounter.text = this.numberFormatter.format(totalCount) binding.totalCounter.text = this.numberFormatter.format(totalCount)
}
override fun exceptionCaught(e: Exception) {
this.exceptions.add(e)
} }
} }

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

Loading…
Cancel
Save