Compare commits

...

948 Commits
csv ... 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
Laurent 95cc8ca329 Added november 2020 flavor 5 years ago
Laurent 6a7a04a9e8 Fixes refresh issues 5 years ago
Laurent b125e94dda Fixes calculation refresh problem 5 years ago
Laurent 50ff357afa Attempt to fix lifecycle crashes with bottom sheets 5 years ago
Laurent 44aad63b82 Bumps version to 5.0.7 / 102 5 years ago
Laurent 5891de3677 Fixes hand created multiple times when going back and forth between editor and replayer 5 years ago
Laurent 115e375434 Fixes look 5 years ago
Laurent a13c185dbb cleanup 5 years ago
Laurent 207eab21a7 reduce padding for smaller screens 5 years ago
Laurent f2b4d29a1f cleanup warnings 5 years ago
Laurent 1dd98ad18a Hand replayer layout improvement 5 years ago
Laurent 6cf42dadd2 Fixes crash for omaha evaluation 5 years ago
Laurent 3e65171c2f Fixes issue where top player chip and pot were over under 5 years ago
Laurent 14210452bf The filter name was not refreshed after a name update + cleanup 5 years ago
Laurent 24cc9327e4 Code improvement 5 years ago
Laurent 8a718de75d Fixes warning + update online hands per hour to the same value as iOS 5 years ago
Laurent caacb95b40 Bumping version to 5.0.6 / 100 5 years ago
Laurent a4d60e9fca cleanup 5 years ago
Laurent 466f7f9ff7 Ignore .aab files 5 years ago
Laurent 6993bc8784 Remove picto gear and put ic_settings instead 5 years ago
Laurent 53dd61086c Fixing crash when sharing before saving a new hand 5 years ago
Laurent a11af4b08e Renaming resource 5 years ago
Laurent f35f7d7596 Remove unused resources 5 years ago
Laurent 0f59569ded output update 5 years ago
Laurent 92f12615d7 use a ligher ffmpeg version 5 years ago
Laurent 35c8ac274d Show/Hide villain cards implementation + drawable cleanup 5 years ago
Laurent f01386347a Fixes geolocation issue + remove image cropping dependency 5 years ago
Laurent ff24fca659 Upgrade coroutine version 5 years ago
Laurent 25a7b087fa Cleanup 5 years ago
Laurent e3aaffc310 Renamed vague HandHistory prefix into Editor where needed 5 years ago
Laurent 4f32bb6df8 Fixes #35 : crash when switching back and forth between the replayer and the editor 5 years ago
Laurent 00086cc9e1 Disable CI 5 years ago
Laurent 9e1eae3a17 Cleanup 5 years ago
Laurent 27b1f46285 Change hero color 5 years ago
Laurent b568ac6487 Fixes a bug where the replayer was disabled 5 years ago
Laurent 280865da43 Refactoring: TableDrawer belongs to ReplayerAnimator 5 years ago
Laurent 62c042cd0e Refactoring 5 years ago
Laurent 7383e4af01 Use slightly lighter ffmpeg lib 5 years ago
Laurent b42bc25c4c Improves video quality + save at better directory 5 years ago
Laurent fa9fe132ea Fixes issue where hh video export last frame did not show up 5 years ago
Laurent cb4509ff15 Removes warning 5 years ago
Laurent cb1ea6680a Fixes the replayer stopping when an export was also made 5 years ago
Laurent 7dafd0b87b Fixes #34 : visual glitches when replaying a hand and exporting at the same time due to the static nature of the drawer 5 years ago
Laurent 2d1589babb Integrate FFMPEG library to manage the video export 5 years ago
Laurent 9aac503fc9 Fixes #33 : pressing back when playing a hand on the replayer 5 years ago
Laurent 0a7f45c711 Added in-app review window 5 years ago
Laurent 0671994bba Upgrade targetSdkVersion from 28 to 29 5 years ago
Laurent b2c11c266d Added GIF export 5 years ago
Laurent cddec1379c Bumps version to 96 / 5.0.2 5 years ago
Laurent a877fae923 Update firebase version 5 years ago
Laurent 326c665f42 Fixes space between pot and cards 5 years ago
Laurent 127906b0f4 Improve crashlytics reports 5 years ago
Laurent f0e666c45d version bump to 95 / 5.0.1 5 years ago
Laurent c007ac1174 Fixes crash if card count is not sufficient for evaluation 5 years ago
Laurent 1d7bf536db viewmodel variables renamed to model 5 years ago
Laurent 97abf8f7fa output update 5 years ago
Laurent 199d38f35f Fixes crash 5 years ago
Laurent 1ce9ee3ee3 Merge hand history branch 5 years ago
Laurent 3d55e5d858 version bump 5 years ago
Laurent c2da834a88 Removes chip distribution animation when the hand uses wildcards 5 years ago
Laurent 3279abcaff Various fixes and improvements 5 years ago
Laurent be479ed0b5 version code bump 5 years ago
Laurent 2268fe46a2 Fixes file provider crashes 5 years ago
Laurent 06d9483054 Get better logs if crash 5 years ago
Laurent 44db39c743 Revert dramatic changes 5 years ago
Laurent c2ad1c9ece Bumps version code 5 years ago
Laurent 7c6cfc70d1 Fixes visual issues with small 2/3 players grid 5 years ago
Laurent 1514ac64e5 Sets version as alpha 5 years ago
Laurent ad974e0eae Fixes possible call negative effective amounts when bet amounts are missing 5 years ago
Laurent a50f645cda Fixes an issue where a call after an amountless bet would show a negative value 5 years ago
Laurent c8f8250484 Fixes #32 5 years ago
Laurent 090a44d34e Fixes number parsing 5 years ago
Laurent 23728de185 Set version to 5.0_beta_1 5 years ago
Laurent a497595756 Cleanup 5 years ago
Laurent 7a64eb1e65 Put video export in a coroutine 5 years ago
Laurent f16defa86e use large heap to avoid video export OOM crashes 5 years ago
Laurent 3f1ef1b599 Cleanup 5 years ago
Laurent da06b2f558 Removed warning 5 years ago
Laurent 380ec9f293 Fixing crash #25 5 years ago
Laurent b7bd5ab64c Refactoring 5 years ago
Laurent 7e19e42014 Cleanup 5 years ago
Laurent 8310712e3d Changes menu icons 5 years ago
Laurent 9499b3e870 Fixes dimensions for 3 and 8 players 5 years ago
Laurent 05e7910e34 Fixes a couple of crash 5 years ago
Laurent 1907b97d59 Create hh export service with notification when done 5 years ago
Laurent a5c95d2131 Refactorgin 5 years ago
Laurent 995c476467 Fixes color issue + cleanup 5 years ago
Laurent 57aed18350 More cleanup 5 years ago
Laurent 6cdbba4e09 cleanup 5 years ago
Laurent 3e1f0345e7 Makes folds faster and the end longer 5 years ago
Laurent 46cff86947 Fixes issues with frame lengthes 5 years ago
Laurent 8c4e297164 Fixes various export and reading issues 5 years ago
Laurent 91b17e6897 Cleanup and bugs fixes 5 years ago
Laurent e1ff129721 chip color changes 5 years ago
Laurent 425d05d436 Fixes crash + visual improvement 5 years ago
Laurent c3f930e339 Frame and animation refactoring 5 years ago
Laurent 1e9d726f23 Changes edit drawable to a smaller one 5 years ago
Laurent 2270304b5e Improves chip placement 5 years ago
Laurent 608dd60136 Remove rosace 5 years ago
Laurent de4e6e45fe Fixes player placement around the table 5 years ago
Laurent 27cf9b3a11 Added april2021 flavor 5 years ago
Laurent f8fccc72cf work in progress: table drawing fucked up, added navigation between hh edit and replay, plus next/forward hh 5 years ago
Laurent d8c6e6cbd5 Fixes memory problems when exporting 5 years ago
Laurent 59d89988ed Fixes dealer button drawing 5 years ago
Laurent f502374967 Video export code, crashing due to memory allocation 5 years ago
Laurent a18e180f07 Refactoring + documenting 5 years ago
Laurent 40d60ff559 Fixes won pot save and replayer end animation 5 years ago
Laurent 37fd11d162 Fixes issue with stacks + show player as allin 5 years ago
Laurent f0787ab8ab Shows player stack at hand start 5 years ago
Laurent 7156f00d82 Shows hero in read mode 5 years ago
Laurent fc69ebce13 Fixes replayer speed 5 years ago
Laurent cf6027e530 Improves and fixes the play/pause button management 5 years ago
Laurent ac196227b2 Fixes frames for street 5 years ago
Laurent 70b1324fdd Chip distribution test and fix 5 years ago
Laurent 2e73896659 Make the code for pot distribution animation 5 years ago
Laurent 4464ae54a3 More chip color + color refactoring 5 years ago
Laurent aea0a48674 Adds various chip colors + fix pot display 5 years ago
Laurent d60dcbb69f Show action amounts 5 years ago
Laurent 51b8482c6e Raises animation fps to 60 5 years ago
Laurent dd7a2d5575 Adds animation when chip goes into the pot 5 years ago
Laurent b94f809d59 cleanup and refactoring 5 years ago
Laurent cb4206ea3a Change replayer drawing algo to draw only what is necessary 5 years ago
Laurent b19fa6823d Fixes first action index 5 years ago
Laurent 39fc5bc5f6 Fixes text clear 5 years ago
Laurent 2d9032c64e Many improvements 5 years ago
Laurent d62285a16e Toolbar improvement + buttons logic 5 years ago
Laurent c32d0cf402 clear logic 5 years ago
Laurent 3ab853c693 Show pot 5 years ago
Laurent 62837f2323 Add board cards 5 years ago
Laurent eb9f435b8b Adjust chip placement 5 years ago
Laurent 0961a74c15 Add chips 6 years ago
Laurent 301ba027f9 update on cards and player names 6 years ago
Laurent 1d9d301ae0 Fixes cards display 6 years ago
Laurent 3ae8cb22c4 Added cards 6 years ago
Laurent b084bca418 Player and stack drawing 6 years ago
Laurent f7315defe4 more archi + first drawn rectangle 6 years ago
Laurent 982ded7e66 Video export early works 6 years ago
Laurent 54de3f4c80 Package refactoring, modules creation 6 years ago
Laurent 3e0f8dcdb2 Version bump 6 years ago
Laurent 030b7cf949 Fixes notif delay 6 years ago
Laurent 272b804fe9 Stop notifications fixes + scheduling/canceling management 6 years ago
Laurent ec2586aadb Add logs 6 years ago
Laurent c5ba4da06c Make notifications work 6 years ago
Laurent ec71899630 Adds stop notification setting 6 years ago
Laurent 192cc7c739 Merge optimal duration branch + adds message in session view 6 years ago
Laurent ae7ad9639f reorg imports 6 years ago
Laurent f605b2ada7 Uninteresting 6 years ago
Laurent fa2e351cfe Remove useless test 6 years ago
Laurent 5e847a3160 Remove useless test 6 years ago
Laurent 3493089ea4 Improvements in the algorithm 6 years ago
Laurent effe1db03e First draft for optimal duration computation, add apache math dependency 6 years ago
Laurent 4c91b27275 Fixes warning and ease the use of Query object 6 years ago
Laurent 5bd89c97af insignificant stuff 6 years ago
Laurent bb25d20f7e cleanup 6 years ago
Laurent d95457ad09 Bumps version code to 86 6 years ago
Laurent ada7ec05b8 Fixes straddle regression + define default number of players to 9 6 years ago
Laurent 968a694df9 Removed retrofit/gson use + fix lint issues 6 years ago
Laurent 8ddcadfb00 updates to currency API, does not work though 6 years ago
Laurent a74833ac5d Fixes visual bug 6 years ago
Laurent 30a0101c44 Improves text export 6 years ago
Laurent 5767d98802 Adds text translation for french 6 years ago
Laurent 50bbdffc8d Removes warnings 6 years ago
Laurent fca74a3d5e Merges master into hh, fix conflicts 6 years ago
Laurent 6b18c8c5d0 Bumps to 2.4.3 6 years ago
Laurent 5d13069392 Bumps version 6 years ago
Laurent 875d37fc27 Protects unsuccesful typeface loading from crashes 6 years ago
Laurent b5bfc875d8 cleanup 6 years ago
Laurent dc41ad36e6 Fixes small issues with transactions and data edition 6 years ago
Laurent f32d8eee3c Small refactoring 6 years ago
Laurent 6d4b17aff3 Bumps version to 82 - 2.4.1 6 years ago
Laurent ffd684774c Fixing exporting issue with comma decimal separator 6 years ago
Laurent 26867cb23c Bumps version code to 79 6 years ago
Laurent 806376db0a Fixing tournament type values 6 years ago
Laurent cdfbf1bf3f Fixes in custom fields creation 6 years ago
Laurent 72d2dbf766 Custom field management in CSV import/export 6 years ago
Laurent d964be2d16 Bumps version to 2.4 6 years ago
Laurent 83dbad0e0f Added Transactions export / import 6 years ago
Laurent 18eb54309e Remove warnings 6 years ago
Laurent 9c2e183b5e Tournament Type was not imported 6 years ago
Laurent 96a4dc9c54 Fixes import issue 6 years ago
Laurent bf3439963b Makes sessions export work 6 years ago
Laurent 0048dfb692 Shows import error message 6 years ago
Laurent eb64f231f2 Upgrades gradle plugin to fix crash 6 years ago
Laurent 51a524116e Sessions CSV export first commit 6 years ago
Laurent 294b30e86d Adds CSV export first bricks 6 years ago
Laurent 594f7d5522 Fixes build issue 6 years ago
Laurent 92e81535cf Fixes translation crash 6 years ago
Laurent b1cf802f2a build upgrades 6 years ago
Laurent 9b2c5f6ee6 Fixes crash when tapping on the ALL button in Filter 6 years ago
Laurent ab4181c633 Removes jan_2020 build flavor 6 years ago
Laurent 5b5abc7ce5 Adds translations 6 years ago
Laurent bc78db7f0d cleanup 6 years ago
Laurent f5bac646b8 Fixes missing translation + typo 6 years ago
Laurent 4a8047bfad Show subscriptions terms of use 6 years ago
Laurent efbf99afc4 Adds verification 6 years ago
Laurent 65f6333d24 Fixes #22 6 years ago
Laurent ba61c89967 Cleanup 6 years ago
Laurent d4697d537e Fixes #21 : unwanted back provokes data loss 6 years ago
Laurent 33dc5aaf1e Use US strings resources for hand actions 6 years ago
Laurent 3da6895516 Fixes #20 6 years ago
Laurent 581efbf84f Cleanup 6 years ago
Laurent d09c7273d9 Puts color for suits in the card selection 6 years ago
Laurent 65e24d3088 Stop forcing the max number of cards if unsure 6 years ago
Laurent c0f51ae2f5 Improved logs 6 years ago
Laurent 5c9106ac50 Adds logs for crash investigation 6 years ago
Laurent f7095db30c Adds missing "this" 6 years ago
Laurent e244158db4 Adds locale logs to better identify a crash 6 years ago
Laurent 943f05126d Fixes #7 : amounts auto validation when selecting somewhere else 6 years ago
Laurent 4d53fd1863 Fixes #19 : issue with the BB acting or not preflop 6 years ago
Laurent b93cd3c73c Change version to 3.0 6 years ago
Laurent a9e2c4037f Change version to 3.0 6 years ago
Laurent 7164f855fb Fixes migration crash with master 6 years ago
Laurent 3d832392b4 Fix and cleanup 6 years ago
Laurent 297cb99cd6 Fixes regression due to the BB walk fix 6 years ago
Laurent 5d439da87a Remove button shadow 6 years ago
Laurent c386cd2842 Removes back / forward from menu 6 years ago
Laurent 6bfbbba581 Fixes #12 + inconsistencies with ante / bba behavior 6 years ago
Laurent 0824ff51d6 Fixes position width for consistency 6 years ago
Laurent 388b9c9539 Fixes #16 : players can be used in multiple positions 6 years ago
Laurent f0ebd16462 Improve logs 6 years ago
Laurent ab1b7a1435 Adds BB walk case, displays summary 6 years ago
Laurent 1ed680b6ce Fixes double fragment issue in dialog mode 6 years ago
Laurent 8e1ba24a51 Add button removed for Session > Hand list, fixes #11 6 years ago
Laurent a1ff6b067a layout fixes 6 years ago
Laurent 6bde91f26b Fixes #7 : Validate player stack if adding a new position row 6 years ago
Laurent 34b622d689 Clears selection when removing players 6 years ago
Laurent 29aaf89e99 Closes #6 : player select on summary crashes 6 years ago
Laurent 7aada36f8a Fixes #5 : close button bad layout 6 years ago
Laurent 881c8def65 Fixes #3: allin amount edit 6 years ago
Laurent a81fc41044 cleanup + comments 6 years ago
Laurent acd97e6a5c Added pot test and fixed issue 6 years ago
Laurent ce5a1d219b Algo to determine pot and split pots 6 years ago
Laurent e71316d70a Put edited values as placeholder 6 years ago
Laurent 23f8fe7949 Refreshes session durations when reopening the app 6 years ago
Laurent 8d62c7b914 Update on won pots 6 years ago
Laurent d8af6e9aaa Bottom sheet refactoring 6 years ago
Laurent ee9a335f22 Adds algo to get winners and their share 6 years ago
Laurent 570cacdffe Filters added ViewModel and navigation refactoring to fix crash 6 years ago
Laurent 5848f7df22 Refactoring to move various fragments and activities into modules 6 years ago
Laurent 098c8014d7 Removes warning 6 years ago
Laurent 8abd8021a0 defines winner positions asynchronously as it takes time 6 years ago
Laurent 1fe06b1931 Realm use optims 6 years ago
Laurent 95f414f6d5 Fixes background issues 6 years ago
Laurent 17bbb7765e Refactors Double.formatted() into a val 6 years ago
Laurent d6ccea472e Fixes hand evaluator issues 6 years ago
Laurent 736bc0d543 Hands evaluator first commit 6 years ago
Laurent 2b8096f209 Changing color 6 years ago
Laurent 45b22ee9fa Fixing vertical alignment 6 years ago
Laurent dc382b8c5b cleanup 6 years ago
Laurent c0ba02220d Hides unavailable video export capabilities 6 years ago
Laurent 4e5cd493c2 Fixes useless card layout hint in read mode 6 years ago
Laurent 6b815cfcb1 Fixes Transactions tab getting on 2 lines 6 years ago
Laurent 83168db3ae Fixing a couple of issues 6 years ago
Laurent 18e68c9225 Cleanup 6 years ago
Laurent 2069be7188 Fixes background issue 6 years ago
Laurent 76264ddabb HH settings are now hidden by default 6 years ago
Laurent 4c122dbd6c Fixes crash by removing ability to clear number of player 6 years ago
Laurent 545e241b8e Fixes crash 6 years ago
Laurent 5d3035667b change read mode layout 6 years ago
Laurent 024ea38b52 background for amount edittext 6 years ago
Laurent d004781edb Fixes issues with allins & player stack 6 years ago
Laurent c4022d9b15 better code 6 years ago
Laurent 2bf6972bea Fixes issue with stack changes 6 years ago
Laurent bbd4d3640a Fixes issue with remaining stacks 6 years ago
Laurent 8cace5259a Fixes issues where allining a player was displaying the summary 6 years ago
Laurent 208ed54084 Upgrade firebase 6 years ago
Laurent 9f00a2e427 Removing broser dependency + updating blog URL 6 years ago
Laurent a363c4a0e5 Reorder items + cleanup 6 years ago
Laurent 3c6424e95b Fixes stuff on player setup removeal 6 years ago
Laurent aebdc18bd4 Adds ability to remove a player setup 6 years ago
Laurent 54163d7307 cleanup 6 years ago
Laurent 1e6de627a7 Autoselect bankroll for transaction when only one exists 6 years ago
Laurent 03ce732b95 Fixes UI 6 years ago
Laurent 10410cb245 Fixes popup background 6 years ago
Laurent 5e85c672cb Fixes button color 6 years ago
Laurent a957f7e2e9 Upgrade material to 1.1.0 6 years ago
Laurent 971f2bcb12 cleanup 6 years ago
Laurent 2c4acfdecc Feed: Change hands grouping by day 6 years ago
Laurent 30f2c2cf9b Remove warnings 6 years ago
Laurent 9f4a365a9a UI improvement 6 years ago
Laurent a64881aa1c Removed warning 6 years ago
Laurent 17d788deb1 cleanup 6 years ago
Laurent 581c4936fa Add rule to avoid having both ante and BB ante at the same time 6 years ago
Laurent be9bcc8ceb Fixes BB ante in read mode 6 years ago
Laurent f32488bb64 Fix hand pot color 6 years ago
Laurent 5e6d18cfdd Improved readability 6 years ago
Laurent 4794c4c810 Add comments 6 years ago
Laurent 057be14508 Text export improvement 6 years ago
Laurent 8b6c1d0ee2 cleanup + crash fix 6 years ago
Laurent f4a4f7786e Make edit/save work as expected 6 years ago
Laurent 76c24895c1 Update strings 6 years ago
Laurent ba7c29a7f9 Improve read mode UI 6 years ago
Laurent 64d3bf9d58 UI improvement 6 years ago
Laurent eb0cced0a3 Fixes header colors 6 years ago
Laurent ef4b7c9708 Do not show comment in read only if empty 6 years ago
Laurent 5dae5e4487 Upgrade core-ktx library 6 years ago
Laurent 7154100c1e Fixes layout 6 years ago
Laurent dbf749f97e Fixes build 6 years ago
Laurent 4ca7e1c201 Crash fix + card layout in edit mode 6 years ago
Laurent 1338e21da1 Highlights hero 6 years ago
Laurent 65015641ba Various improvements 6 years ago
Laurent 5f30b844dc cleanup, improvement and bug fix 6 years ago
Laurent 223ca8ea18 UI improvements 6 years ago
Laurent 46c20541bf Fixes card hints 6 years ago
Laurent 3f24ebdeed Remove card layout hint for the board 6 years ago
Laurent efda21b684 Fixes bug + improvement with position shortcut 6 years ago
Laurent 2c960078fa Fixes color 6 years ago
Laurent 455b7ca218 Fixes crash 6 years ago
Laurent 85660bf7bd Adds hint to card layouts 6 years ago
Laurent cae36ce9f7 Fixes crash 6 years ago
Laurent 64a60bdd7d Fixes header background color 6 years ago
Laurent 6770291aa2 cleanup 6 years ago
Laurent 12ea787046 Fixes cards width 6 years ago
Laurent fe56e56cac Make place for 5 cards 6 years ago
Laurent 883a799892 cleanup 6 years ago
Laurent 814d825373 Start of UI work 6 years ago
Laurent 52b25af5e7 cleanup 6 years ago
Laurent 85a4972212 Fixing layout 6 years ago
Laurent 8dabd8931f Adds POT action 6 years ago
Laurent 057f15be55 cleanup 6 years ago
Laurent 1e73c0ec4b Layout improvements 6 years ago
Laurent e29e60576a Fixes bugs + improvements 6 years ago
Laurent 3acb0f02ef Refactor the way PlayerSetups are added 6 years ago
Laurent eb049a8ed2 Changes the PlayerSetup display, always shows the hero position 6 years ago
Laurent eaafa7b5e9 Fixes stuff 6 years ago
Laurent 97b5c8f694 Removed warning 6 years ago
Laurent 898304f94b Fixes crash in Feed > hands 6 years ago
Laurent ec402c8d17 Fixes warnings 6 years ago
Laurent 93eebe40b2 Puts undefined value or suit if not available 6 years ago
Laurent 74da03e7f8 Fixes issue with card layout selection + removes the card limit when a user edits a non-empty card selection 6 years ago
Laurent 796ace74e3 Fixes player stroke issue 6 years ago
Laurent 5d048015d8 Fixing crashes when adding a new player 6 years ago
Laurent 1a56c245b6 Cleanup 6 years ago
Laurent d8d8aa8441 Fixes crash due to gradle upgrade 6 years ago
Laurent 105f56ce0a Player selection improvements 6 years ago
Laurent bbc7d154ca View hands from Session 6 years ago
Laurent 4ff41f6391 Removes useless hands field from Session + fix build 6 years ago
Laurent 5bfdee584f Fix warning 6 years ago
Laurent ad6828e333 HH configuration from Feed or Session 6 years ago
Laurent fa69fcb264 Adds configurationId to create a HH from a session or a previous HH 6 years ago
Laurent ea6b0a29d5 Progress made on the player selection 6 years ago
Laurent 44d1c5fa03 Adds method documentation 6 years ago
Laurent d66a706c07 Adds onItemClick for RowRepresentableDelegate 6 years ago
Laurent e6d1160e35 Cleanup 6 years ago
Laurent 88ebaba62f Adds Tag for Player button + stuff 6 years ago
Laurent bc21a39e9a Fixing player setups read only issue + read rows improvement 6 years ago
Laurent 774ae12f6f Fixes hand rows in the feed adapter 6 years ago
Laurent 22daa6005e Disable stuff when hand is not edited 6 years ago
Laurent dbe325e6e7 read mode for player setups 6 years ago
Laurent 111b7a7489 Fix build 6 years ago
Laurent fc43bc75c2 Adds compacting for text export 6 years ago
Laurent d986378ffe Create the ActionReadRow and the extension to compact a list of ComputedAction into a list of ActionReadRow 6 years ago
Laurent 95d8e0e229 Fixes crash when selecting number of players 6 years ago
Laurent 6702ea04f7 Fixes selection of board views 6 years ago
Laurent 93f17ef522 Automatically defines a minimum amount if the user validates an empty amount 6 years ago
Laurent f48d79e0ad Fixes minimum bet possible amount 6 years ago
Laurent c7433d6702 Corrects the min possible bet amounts 6 years ago
Laurent 2da1f6f410 Remaining stack calculations 6 years ago
Laurent 3865346b2b Update remaining stacks when updating antes 6 years ago
Laurent 2396150123 Make ante effective 6 years ago
Laurent b2ecf5ebc7 Refactors editDescriptors to return a list instead of an arraylist 6 years ago
Laurent d6abbb3579 Split blinds row into ante + BB ante 6 years ago
Laurent 98743fd365 Adds Session.Type as a HandSetup parameter to remove Straddle parameters 6 years ago
Laurent c05f97df2d Fixes board selection issue 6 years ago
Laurent 2a00e529d5 Use cute cards for board and players 6 years ago
Laurent 12dc039a1b Creates HH row for the feed 6 years ago
Laurent 6b15412b45 Work in progress 6 years ago
Laurent 3c81d9dc34 filterAdapter first draft 6 years ago
Laurent 565223ad5a Fixes missing HH case 6 years ago
Laurent 2f45fee417 Change sharing button + improve text export 6 years ago
Laurent 1cd4dfc229 Adds comment to text export 6 years ago
Laurent 5815a36649 Removes complications with straddle adding 6 years ago
Laurent 889f347969 Manages clear for amounts 6 years ago
Laurent ef52c4a071 Adds export button + HH text export 6 years ago
Laurent 457cb4f95e Creates menu options + blinds auto update 6 years ago
Laurent 2aa63f8400 Improves keyboard layout 6 years ago
Laurent 61e77a55c6 Improve keyboard animation 6 years ago
Laurent d4dd65a09d Start of edit/save + blind setting from the actions 6 years ago
Laurent 82ce3c76b3 Fixes straddle header having a value 6 years ago
Laurent 271600f357 Adds missing touch listeners 6 years ago
Laurent 12495533b3 Fixes cursor placement 6 years ago
Laurent eaa5b87c13 Fixes focus issue on keyboard close 6 years ago
Laurent d9e52e865f Fixes issue with positions selection 6 years ago
Laurent 71b633c259 Improvements on the card/board selection 6 years ago
Laurent 4b2638a1dc Fixes autoselection issue 6 years ago
Laurent 4cd018854c Fixes issues where Next would not change the focus 6 years ago
Laurent e88700b36b Fixes warning 6 years ago
Laurent 0a603bab1a Fixes various position issues 6 years ago
Laurent 9254c40425 Fixes various bugs 6 years ago
Laurent 4092fdedef Refactors stringForRow into charSequenceForRow 6 years ago
Laurent 524520acd1 Fixes another crash 6 years ago
Laurent 0025f29bc1 Fixes crash 6 years ago
Laurent 06c9fde229 Fixes crashes 6 years ago
Laurent d5bc0309aa Factor the way EditText are handled 6 years ago
Laurent 7411aa460b Progression in Player Setups 6 years ago
Laurent 40e84c8a96 Update for player setup + realm 6 years ago
Laurent e7b820dc33 Prepare Player Setup rows 6 years ago
Laurent 64d593df41 Enables number of player selection 6 years ago
Laurent 313b27e16a Added the ability to edit comment 6 years ago
Laurent f4acb0f805 Cleanup keyboard display + fix missing positions shortcuts after flop 6 years ago
Laurent 85fdbdaa9d Fixes various card selection issues 6 years ago
Laurent 0a2870b6d9 Fixes straddle edition issues 6 years ago
Laurent d10bd13379 Manages SB + BB 6 years ago
Laurent 534809e3e9 Adds todo 6 years ago
Laurent 3ceba43ded Fixes straddle position bug + display 6 years ago
Laurent cc2a7fbbec Adds blinds + straddle rows 6 years ago
Laurent 08012fc88e Adds HH settings initial stuff 6 years ago
Laurent 2a78b7e386 Some light refactoring 6 years ago
Laurent cb055bcaa3 Cleanup 6 years ago
Laurent 46089b13c6 Manages card storage 6 years ago
Laurent 1d30f51e3c Puts HHBuilder into the HHViewModel + cleanup + doc 6 years ago
Laurent b203763882 Big Refactoring: Splitting HHBuilder into HHBuilder + ActionList 6 years ago
Laurent f8ab6489c5 Refactoring + issue fix 6 years ago
Laurent 6a72abf674 Fixes 6 years ago
Laurent 6fd6eb43e5 Fixes pot size issue 6 years ago
Laurent 5924a8876a Fixes issue with SUMMARY 6 years ago
Laurent b522dc80ff Fixes bug with checks 6 years ago
Laurent f456fbd3f2 Fixes pot calculation 6 years ago
Laurent a93e2da0e8 Fixes action dropping issues 6 years ago
Laurent 9143bf4e10 Manages pot size for street headers 6 years ago
Laurent 0ab62bfedb Fixes the nasty hiding EditText content 6 years ago
Laurent 3c304f3cd5 Improvements 6 years ago
Laurent 1c365d9dbb Fixes build 6 years ago
Laurent b98c0c3a38 Keyboard improvement + doc 6 years ago
Laurent edc9b5d6b4 Cleanup 6 years ago
Laurent 8d2a6a4033 Improve style + cleanup 6 years ago
Laurent df637d64a5 More cleanup 6 years ago
Laurent 028d6eaf65 Cleanup 6 years ago
Laurent 6976aa7c63 Fixes various bugs 6 years ago
Laurent f61a7f3806 Fixes actions for the blinds when acting again without new significant action 6 years ago
Laurent 024ce91040 Various fixes and improvements 6 years ago
Laurent 808006e074 Disable components when necessary 6 years ago
Laurent 7082ef7c23 More and more progress 6 years ago
Laurent 9ba73dc7c1 More HH progress 6 years ago
Laurent d743cda1f0 Adds documentation 6 years ago
Laurent f775b60f88 Adds Player rows at SUMMARY + fixes 6 years ago
Laurent 2c30a32e2e Automatically manages new actions + street creation 6 years ago
Laurent 2d2a7aa446 Advances on card keyboard and automatic action creation 6 years ago
Laurent 66730eb43e Work on board rows 6 years ago
Laurent 70df3195e5 Adding method prototype 6 years ago
Laurent a5fb83fbe1 Makes the position shorcuts work 6 years ago
Laurent d24ae60dd5 Adds position selection in the Action keyboard - unfinished 6 years ago
Laurent 658792e93a Fixes keyboard and amount edition issues 6 years ago
Laurent c206354dae keyboards refactoring 6 years ago
Laurent 20f8151394 Manages table refresh when actions drop occur 6 years ago
Laurent 716586782e Adds row refresh of automatically set actions 6 years ago
Laurent 3287bdff0c Fixes row refresh 6 years ago
Laurent 67caf7a125 Fixes crash 6 years ago
Laurent d8a6de3e94 Put hand history related classes in a handhistory package 6 years ago
Laurent 4c91718967 Custom numeric keyboard management - unfinished 6 years ago
Laurent e7782599e1 Fixing todos in the HHBuilder 6 years ago
Laurent 39709e3e97 keyboard changes 6 years ago
Laurent 529a7cc21e First implementation of numeric keyboard 6 years ago
Laurent 763fb331f9 Show/hide soft input, i.e. keyboard 6 years ago
Laurent 67155d382a Fixes issues with editText focus 6 years ago
Laurent 5799a17fa9 HH progress 6 years ago
Laurent 506c4bd931 Fixes bug in Builder 6 years ago
Laurent b1fa2d9020 Various refactoring to fix text edition bugs 6 years ago
Laurent 1c9932cd32 HH progression 6 years ago
Laurent c0c28c628f Fixes issues with actions chaining 6 years ago
Laurent bf9223a3f3 More improvements on HH 6 years ago
Laurent b5f238d165 Fixes build 6 years ago
Laurent f624546598 Refactored onRowSelected: change fromAction:Boolean to tag:Int to allow more flexible usages + HH progression 6 years ago
Laurent e437591748 Adds various keyboards 6 years ago
Laurent 5c2542f758 Progression on Hand History UI 6 years ago
Laurent acf77888a4 Fixes realm crash 6 years ago
Laurent b738447f06 Defines view type for Street Header 6 years ago
Laurent d604e3371f Better error handling 6 years ago
Laurent df13c43e3d Adds UI elements to HH 6 years ago
Laurent 42001c8f0d Refactoring + todos 6 years ago
Laurent 15db3c4d3a Adds basic UI for hand history 6 years ago
Laurent 62c24590f0 Fixes issue + refactoring + doc + cleanup 6 years ago
Laurent c1e09c41eb Refactoring + implementation of dropNextActionsIfNecessary 6 years ago
Laurent 8dcf26815d Added more hand history case management 6 years ago
Laurent 9da80a7222 Working video test with bitmap + canvas 6 years ago
Laurent 296b7942b0 Adding new HH button linked to video test 6 years ago
Laurent 4fe79df786 Adds HH migration 6 years ago
Laurent ad1c11bcfe Adds documentation 6 years ago
Laurent 884ae51f3c Improved lisibility 6 years ago
Laurent 6cee11edbe Manage forgotten case to determine possible HH player action 6 years ago
Laurent e321014973 Adds View extension to convert View into ByteArray 6 years ago
Laurent b942c12d12 Adds algorithm to determine the user available actions 6 years ago
Laurent fb56773f64 Moved MediaMuxer class 6 years ago
Laurent cf3a8b2f16 Adds MMediaMuxer to assemble images into video 6 years ago
Laurent ef362bfa28 Added creation of new hand, building of actions per street 6 years ago
Laurent 1c2a4d0a09 First hand history commit 6 years ago
Laurent 73b5fbcc4d Fixes issues with Year criteria 6 years ago
Laurent f72c15c926 Fixes issue where a pending session was messing with the month criteria 6 years ago
Laurent d4b39c1f14 Gradle plugin update 6 years ago
Laurent ca263b226f Fixes crash when a Session without StartDate was caught in the Bankroll Report 6 years ago
Laurent c87b828cdf Fixes init crash 6 years ago
Laurent 1261a5505f Adds locale crashlytics log 6 years ago
Laurent b7a4b313b6 Refactoring: use arguments + bundle to pass data to the fragment 6 years ago
Laurent 31b9607284 Fixes lifecycle crash on DataManagerFragment 6 years ago
Laurent 62503c0c2d Adds home message for discord + upgrade of the messaging system to add an action button 6 years ago
Laurent a508604b2a Fixing warning 6 years ago
Laurent b4991dcbb1 Adds discord row 6 years ago
Laurent ab55fc951f Bumps version to 2.3.6 6 years ago
Laurent f60fc11165 Temporary remove of Poker Rumble 6 years ago
Laurent 28ab37dd88 Fixes parsing crash 6 years ago
Laurent d15926e108 Adds Poker Rumble 6 years ago
Laurent 2007cc551e Refactored PokerAnalytics[Activity + Fragment] into Base[...] + cleanup 6 years ago
Laurent d97652233b Fixes a crash where the write external storage permission was required 6 years ago
Laurent 7d5f63f30b Fixes BottomSheetFragment crash 6 years ago
Laurent 2d77f1ff89 avoid crash when font does not load, sending crashlytics log though 6 years ago
Laurent aff2bacae6 Sets added viewModel as private 6 years ago
Laurent 6c9dd438a8 Bumps version to 2.3.4 6 years ago
Laurent 1802dea89f Use ViewModel for CalendarDetails + fixes tab selection bug 6 years ago
Laurent 3f75898cc7 Use ViewModel for GraphFragment 6 years ago
Laurent 91f8b11cce Refactoring + cleanup 6 years ago
Laurent 5fe20f8f0b Could fix crash 6 years ago
Laurent da52e7d535 Use ViewModel in SessionFragment / SessionActivity 6 years ago
Laurent 1f774a7b30 Fixes typo 6 years ago
Laurent 8a659a4c8d Bumps version to 2.3.3 6 years ago
Laurent 763efb13a9 Should fix the impossible ViewModel with complex parameter issue 6 years ago
Laurent 0b20482b21 Gradle upgrade 6 years ago
Laurent 1bbbd7c216 Fixes missing error case 6 years ago
Laurent 413b10a4b8 Bumps version 6 years ago
Laurent d460f1c6dc add tests + cleanup 6 years ago
Laurent 8ab999e0f6 Fixes issue with Poker Agent CSV import + bumps version 6 years ago
Laurent 57a4af9619 Fixes rotation Oreo crash 6 years ago
Laurent 27c7c17570 Fixes migration issues 6 years ago
Laurent 1db44d5723 Adds crash logs when exception is caught 6 years ago
Laurent 6dc38880a9 Allows decimal for tip inputs 6 years ago
Laurent 6f539a277f Adds Player feature 6 years ago
Laurent b061038186 bumped version 6 years ago
Laurent 4aa26e9325 added crash log 6 years ago
Laurent c87de6a601 Fixes lifecycle crash 6 years ago
Laurent 52b43cddf8 Attempts to fix lifecycle crash 6 years ago
Laurent 72386d0dc6 Fixes a crash 6 years ago
Laurent 947c865e8b Throws exception if the currency code is wrong when setting it 6 years ago
Laurent 374c65871c Separates messages when product request fails + additional information 6 years ago
Laurent 1fcea0946a bumps to 2.2.9 - 67 6 years ago
Laurent 352340787a Added crashlog for null stat 6 years ago
Laurent 81bf2ba6ff Better Query display name 6 years ago
Laurent ea99385835 Refactoring of bottom sheet to introduce the use of a ViewModel 6 years ago
Laurent daf567e716 Fixes a crash when creating a custom progress reprot 6 years ago
Laurent 909c9d8b50 bumps to 2.2.8 - 65 6 years ago
Laurent 10c601281d Use viewmodel for reports 6 years ago
Laurent 7df72426c4 fixes crash due to unitialized lateinit property 6 years ago
Laurent 58c6838949 Cleanup + fix attempt of report crashing on resuming 6 years ago
Laurent ad9aff52a9 bumps version to 63 - 2.2.6 6 years ago
Laurent 829feb1353 Allow decimals for tournaments 6 years ago
Laurent b03a0822d9 Fixes crash 6 years ago
Laurent 56f2793b0a bumps version to 2.2.4 6 years ago
Laurent 9a3b261abf Fixes crash by adding empty constructor to fragment 6 years ago
Laurent d03f75bff1 Removes another warning 6 years ago
Laurent 22df145fc5 Removes warning 6 years ago
Laurent 8584c765b2 Updated .gitignore 6 years ago
Laurent afcf49ed4c Remove tracking of app/standard/release/output.json 6 years ago
Laurent 96df3a0ae6 Adds june 2020 flavor + version bump 6 years ago
Laurent 0d00d8f447 Adds patch to fix Session Sets potential duration issue 6 years ago
Laurent 259fec137d Fixes crash occuring when the activity is immediately stopped, before each of its child could be instantiated 6 years ago
Laurent df4b059dff Throw exception with missing parameters 6 years ago
Laurent 13485a8cc4 Bumping to 2.2.2 6 years ago
Laurent 7fc35cae72 Fixes crash where deposits transaction type does not exists 6 years ago
Laurent 2dd87b1484 Fixing an issue when session set did not split 6 years ago
Laurent 39a27c011d Bumped version 6 years ago
Laurent 4b7af54399 Fixes issue where the displayed date was the creationDate, not the startDate 6 years ago
Laurent 1a0c89e7cf Fixes potential issue 6 years ago
Laurent 910b3b368b Fixes Break parsing 6 years ago
Laurent cd4de7318d Attempt at fixing crash 6 years ago
Laurent 26042a998d Bumping version number to 2.2 6 years ago
Laurent 5dee5b8d56 Adds helper for parsing numbers, providing an optional 6 years ago
Laurent 26d6ad46a4 Added more limit parsing optiosn 6 years ago
Laurent 37eadeff8b Fixing build 6 years ago
Laurent ffb4c3aa08 Remove gradle warning 6 years ago
Laurent f6c8346bc3 Fixes build 6 years ago
Laurent 320f56c652 Integrating poker analytics iOS CSV import 6 years ago
Laurent b969039597 gradle update 6 years ago
Laurent d7c33db581 Added Poker Analytics CSV source 6 years ago
Laurent 8d896f6b51 Commenting lint failing target 6 years ago
Laurent b77728b37b Remove dirty code 6 years ago
Laurent 0003f38f23 Catches exception when import fails and show error message 6 years ago
Laurent 367dfa7ccb Poker Agent import improvement 6 years ago
Laurent 6d3cb90755 Updating Poker Agent import 6 years ago
Laurent 1a6e39cef1 Adds PokerAgent import 6 years ago
Laurent e45121e575 Added january 2020 flavor 6 years ago
Laurent 368141fcf1 Bumping to 2.1.6 / 56 6 years ago
Laurent 216e60990a gradle upgrade 6 years ago
Laurent 74dd939966 Fix crashes 6 years ago
Laurent 73914c7fd6 cleanup 6 years ago
Laurent 9f4617e1d1 Adds build flavor with endofuse parameter to ease the creation of limited use versions 6 years ago
Laurent 561bfd8b1c Fixes issue with net computation for online tournaments 6 years ago
Laurent 58b0077cca Bump to 2.1.5 6 years ago
Laurent 02ebf78188 Simplifies duplicate menu system and possibly fixes a crash 6 years ago
Laurent 84e985e120 Adding log for crash info 6 years ago
Laurent 9cb6688cd7 Improves logs 6 years ago
Laurent cc6f0ba6a3 Improves activity lifecycle logs 6 years ago
Laurent 9645423e1b Bumps version to 2.1.5 / 52 for update 6 years ago
Laurent 204745a8bf Fixes build 6 years ago
Laurent 45fcdce179 Removed useless code 6 years ago
Laurent a615fbec35 pass liveDataType as protected 6 years ago
Laurent 82a97660b6 Potentially fixes crash 6 years ago
Laurent c44b3f3d2d Fixes an issue where launching 2 reports was possible making the UI blocked 6 years ago
Laurent 17c050b6d8 Create a defaut white text style and set it to the loader dialog 6 years ago
Laurent 8eabda0925 Bumps version to 50 / 2.1.4 6 years ago
Laurent ec66cf5a74 Removes right label for average 6 years ago
Laurent 43beb91bdf Improves fake data generation 6 years ago
Laurent d1cc3a8ea6 Moved ComputedStat in its own class 6 years ago
Laurent 7ded5ad456 Possibly fixes a crash! 6 years ago
Laurent caac54666a Removes fragment classes from obfuscation to hopefully fix crash 6 years ago
Laurent 2344e010cf Possibly fixes UI crash 6 years ago
Laurent ef0cc94b53 Possibly fixes crash 6 years ago
Laurent 8e7038028b Fixes occasional crash in bankroll 6 years ago
Laurent 8b673674ec Fixes obfuscation crash + version bump 6 years ago
  1. 7
      .gitignore
  2. 139
      CLAUDE.md
  3. 138
      app/build.gradle
  4. 24
      app/google-services.json
  5. 28
      app/proguard-rules.pro
  6. 19
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/FavoriteSessionUnitTest.kt
  7. 57
      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. 432
      app/src/main/AndroidManifest.xml
  14. BIN
      app/src/main/ic_launcher-playstore.png
  15. 80
      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. 96
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  19. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  20. 10
      app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt
  21. 171
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  22. 111
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  23. 35
      app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt
  24. 271
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  25. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  26. 107
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  27. 44
      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. 4
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReportManager.kt
  30. 12
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/AggregationTypeExtensions.kt
  31. 84
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ComputedResultsExtensions.kt
  32. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportDisplay.kt
  33. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportExtensions.kt
  34. 81
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/StatRepresentable.kt
  35. 183
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  36. 24
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  37. 54
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  38. 21
      app/src/main/java/net/pokeranalytics/android/model/Limit.kt
  39. 54
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  40. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  41. 13
      app/src/main/java/net/pokeranalytics/android/model/Stakes.kt
  42. 30
      app/src/main/java/net/pokeranalytics/android/model/TableSize.kt
  43. 12
      app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt
  44. 13
      app/src/main/java/net/pokeranalytics/android/model/blogpost/BlogPost.kt
  45. 66
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  46. 8
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  47. 102
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  48. 518
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  49. 108
      app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt
  50. 53
      app/src/main/java/net/pokeranalytics/android/model/handhistory/Position.kt
  51. 50
      app/src/main/java/net/pokeranalytics/android/model/handhistory/Street.kt
  52. 14
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  53. 189
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  54. 8
      app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt
  55. 2
      app/src/main/java/net/pokeranalytics/android/model/interfaces/UsageCountable.kt
  56. 202
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  57. 207
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  58. 85
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  59. 79
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  60. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  61. 12
      app/src/main/java/net/pokeranalytics/android/model/realm/Currency.kt
  62. 132
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  63. 3
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  64. 68
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  65. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  66. 54
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  67. 16
      app/src/main/java/net/pokeranalytics/android/model/realm/HandHistory.kt
  68. 5
      app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt
  69. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  70. 94
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  71. 33
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  72. 64
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  73. 822
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  74. 18
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  75. 22
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  76. 20
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt
  77. 103
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  78. 48
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  79. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  80. 252
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt
  81. 292
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt
  82. 775
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  83. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/PlayerSetup.kt
  84. 17
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt
  85. 14
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/BankrollRow.kt
  86. 57
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/CustomFieldRow.kt
  87. 11
      app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt
  88. 15
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  89. 33
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  90. 20
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  91. 39
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  92. 11
      app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt
  93. 86
      app/src/main/java/net/pokeranalytics/android/ui/activity/ColorPickerActivity.kt
  94. 4
      app/src/main/java/net/pokeranalytics/android/ui/activity/ComparisonChartActivity.kt
  95. 15
      app/src/main/java/net/pokeranalytics/android/ui/activity/ComparisonReportActivity.kt
  96. 4
      app/src/main/java/net/pokeranalytics/android/ui/activity/CurrenciesActivity.kt
  97. 58
      app/src/main/java/net/pokeranalytics/android/ui/activity/DataListActivity.kt
  98. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  99. 71
      app/src/main/java/net/pokeranalytics/android/ui/activity/FilterDetailsActivity.kt
  100. 74
      app/src/main/java/net/pokeranalytics/android/ui/activity/FiltersActivity.kt
  101. Some files were not shown because too many files have changed in this diff Show More

7
.gitignore vendored

@ -6,8 +6,11 @@
# Built application files
*.apk
*.ap_
*.aab
/app/release
app/*2020/
# Files for the Dalvik VM
*.dex
@ -15,7 +18,7 @@
*.class
# Generated files
bin/
bin
gen/
# Gradle files
@ -42,4 +45,4 @@ gen-external-apklibs
# Libraries
/libs/*/*/build
/captures
/captures

@ -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,53 +1,61 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services' // Crashlytics
// Crashlytics
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
repositories {
maven { url 'https://maven.fabric.io/public' }
maven { url 'https://jitpack.io' } // required for MPAndroidChart
jcenter() // for kotlin serialization
}
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
compileSdkVersion 35
buildToolsVersion "30.0.3"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
disable 'MissingTranslation'
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 28
versionCode 47
versionName "2.1.1"
targetSdkVersion 35
versionCode 180
versionName "6.0.38"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
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 {
useProguard false
minifyEnabled true
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationVariants.all { variant ->
variant.outputs.all { output ->
def date = new Date()
def formattedDate = date.format('yyMMdd_HHmm')
def appName = "PokerAnalytics"
def buildType = variant.variantData.variantConfiguration.buildType.name
def buildType = variant.buildType.name
def newName
if (buildType == 'debug'){
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk"
@ -59,6 +67,17 @@ android {
}
}
}
flavorDimensions 'endOfUse'
productFlavors { // already used: 50000, 51000, 52000, 52130, 52110, 52120
standard {
dimension = 'endOfUse'
}
// oct2021 {
// dimension = 'endOfUse'
// versionNameSuffix = '_oct2021'
// versionCode = 52120 + android.defaultConfig.versionCode
// }
}
configurations {
release {
@ -66,44 +85,50 @@ android {
}
}
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
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-json:1.4.1"
// Android
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.2.0-alpha02'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places
implementation 'com.google.android.libraries.places:places:1.1.0'
implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions
// WARNING FOR 2.0: https://developer.android.com/google/play/billing/billing_library_releases_notes
// Purchases MUST BE ACKNOWLEDGED
implementation 'com.android.billingclient:billing:1.2.2'
// Firebase
implementation 'com.google.firebase:firebase-core:17.0.0'
implementation 'com.android.billingclient:billing:7.0.0'
// Crashlytics
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0')
// Declare the dependencies for the Crashlytics and Analytics libraries
// When using the BoM, you don't specify versions in Firebase library dependencies
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// Logs
implementation 'com.jakewharton.timber:timber:4.7.1'
@ -112,18 +137,47 @@ dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv
implementation 'org.apache.commons:commons-csv:1.6'
implementation 'org.apache.commons:commons-csv:1.7'
// Polynomial Regression
implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
// Image picking and registerForActivityResult
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation "androidx.fragment:fragment-ktx:1.4.1"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
// Test
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2'
// gross, somehow needed to make the stop notif work
implementation 'com.google.guava:guava:27.0.1-android'
}

@ -8,20 +8,12 @@
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:245968016816:android:47f8b4f74b1296b4",
"mobilesdk_app_id": "1:245968016816:android:e5597a41d79df0a31d7275",
"android_client_info": {
"package_name": "net.pokeranalytics.android"
}
},
"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_type": 3
@ -33,20 +25,20 @@
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "245968016816-8sklai9sq70m46anv550uttdic6cukn6.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "stax.SlashPoker.nosebleed"
}
}
]
},
"ads_service": {
"status": 2
}
}
}

@ -29,6 +29,10 @@
-dontwarn javax.**
-dontwarn io.realm.**
-keep class net.pokeranalytics.android.model.** { *; }
-keep class net.pokeranalytics.android.ui.fragment.** { *; }
-keep class net.pokeranalytics.android.ui.modules.** { *; }
-keepattributes SourceFile,LineNumberTable # Keep file names and line numbers.
-keep public class * extends java.lang.Exception # Optional: Keep custom exceptions.
# Retrofit
@ -60,4 +64,26 @@
-keep class com.google.j2objc.annotations.** { *; }
# Enum
-optimizations !class/unboxing/enum
-optimizations !class/unboxing/enum
# Serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class net.pokeranalytics.android.**$$serializer { *; }
-keepclassmembers class net.pokeranalytics.android.** {
*** Companion;
}
-keepclasseswithmembers class net.pokeranalytics.android.** {
kotlinx.serialization.KSerializer serializer(...);
}

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

@ -44,8 +44,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300
s1.cgBigBlind = 0.5 // bb net result = -200bb
s2.cgBigBlind = 2.0 // bb net result = 150bb
s1.cgBlinds = "0.5" // bb net result = -200bb
s2.cgBlinds = "2.0" // bb net result = 150bb
s2.tableSize = 5
@ -186,13 +186,13 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
Assert.fail("No std100 stat")
}
results.computedStat(Stat.MAXIMUM_NETRESULT)?.let {
results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let {
assertEquals(300.0, it.value, delta)
} ?: run {
Assert.fail("No MAXIMUM_NETRESULT")
}
results.computedStat(Stat.MINIMUM_NETRESULT)?.let {
results.computedStat(Stat.MINIMUM_NET_RESULT)?.let {
assertEquals(-100.0, it.value, delta)
} ?: run {
Assert.fail("No MINIMUM_NETRESULT")
@ -506,6 +506,55 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
}
@Test
fun testSessionSetCount3() {
val realm = this.mockRealm
realm.executeTransaction {
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("02/1/2019 09:00")
val ed1 = sdf.parse("02/1/2019 10:00")
val sd2 = sdf.parse("01/1/2019 09:00")
val ed2 = sdf.parse("03/1/2019 10:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
val ed22 = sdf.parse("01/1/2019 10:00")
s2.endDate = ed22
}
val sets = realm.where(SessionSet::class.java).findAll()
assertEquals(2, sets.size)
val group = ComputableGroup(Query(), listOf(Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS, Stat.HOURLY_DURATION))
val options = Calculator.Options()
val results: ComputedResults = Calculator.compute(realm, group, options)
results.computedStat(Stat.NUMBER_OF_GAMES)?.let {
assertEquals(2, it.value.toInt())
}
results.computedStat(Stat.NUMBER_OF_SETS)?.let {
assertEquals(2, it.value.toInt())
}
results.computedStat(Stat.HOURLY_DURATION)?.let {
assertEquals(2.0, it.value, 0.001)
}
}
@Test
fun testSessionRestartInOverlappingSessions() {

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition
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.Test
import java.util.*
@ -28,28 +28,25 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
s1.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyBlind()
val filter = QueryCondition.AnyStake()
val blind = QueryCondition.AnyBlind().apply {
val blind = QueryCondition.AnyStake().apply {
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)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -74,33 +71,27 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
s1.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyBlind()
val filter = QueryCondition.AnyStake()
val blind1 = QueryCondition.AnyBlind().apply {
val blind1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
val blind2 = QueryCondition.AnyBlind().apply {
val blind2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.blinds!!)
}
blind1.filterSectionRow = FilterSectionRow.Blind
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElements = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Stakes)
filter.updateValueBy(filterElements)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -125,30 +116,24 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
s1.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyBlind()
val filter = QueryCondition.AnyStake()
val blind = QueryCondition.AnyBlind().apply {
val blind = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s3.blinds!!)
}
blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -172,34 +157,27 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
s1.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBigBlind = 2.0
s2.cgSmallBlind = 1.0
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val filter = QueryCondition.AnyBlind()
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
val stake1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.cgStakes!!)
}
val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!)
val stake2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.cgStakes!!)
}
blind1.filterSectionRow = FilterSectionRow.Blind
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
val filterElement = FilterCondition(arrayListOf(stake1, stake2), FilterSectionRow.Stakes)
filter.updateValueBy(filterElement)
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.FilterCondition
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.startOfDay
import org.junit.Assert
@ -35,8 +35,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) }
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -64,8 +63,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) }
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -92,8 +90,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyYear()
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) }
filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filter.updateValueBy(filterElement)
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)
realm.commitTransaction()
val filter = QueryCondition.StartedFromDate()
val filterElementRow = QueryCondition.StartedFromDate().apply { singleValue = s2.startDate!!}
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filter = QueryCondition.StartedFromDate(Date())
val filterElementRow = QueryCondition.StartedFromDate(Date()).apply { singleValue = s2.startDate!!}
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -412,10 +408,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.StartedToDate()
val filterElementRow = QueryCondition.StartedToDate().apply { singleValue = s1.startDate!! }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filter = QueryCondition.StartedToDate(Date())
val filterElementRow = QueryCondition.StartedToDate(Date()).apply { singleValue = s1.startDate!! }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -441,10 +436,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.EndedFromDate()
val filterElementRow = QueryCondition.EndedFromDate().apply { singleValue = s2.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filter = QueryCondition.EndedFromDate(Date())
val filterElementRow = QueryCondition.EndedFromDate(Date()).apply { singleValue = s2.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -470,10 +464,9 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.EndedToDate()
val filterElementRow = QueryCondition.EndedToDate().apply { singleValue = s1.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filter = QueryCondition.EndedToDate(Date())
val filterElementRow = QueryCondition.EndedToDate(Date()).apply { singleValue = s1.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
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.realm.Filter
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.ui.view.rows.FixedValueFilterItemRow
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@ -24,8 +25,9 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
filter.name = "testSaveLoadCashFilter"
val filterElement = QueryCondition.IsCash
filterElement.filterSectionRow = FilterSectionRow.CashOrTournament
filter.createOrUpdateFilterConditions(arrayListOf(filterElement))
val filterItemRow = FixedValueFilterItemRow(filterElement, FilterSectionRow.CashOrTournament)
filter.createOrUpdateFilterConditions(arrayListOf(filterItemRow))
val useCount = filter.countBy(FilterCategoryRow.GENERAL)
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.QueryCondition
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.Test
import org.junit.runner.RunWith
@ -110,8 +110,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Bankroll))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -141,11 +140,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
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))
@ -169,7 +166,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
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))
Assert.assertEquals(1, sessions.size)
@ -197,10 +194,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) }
filterElementRow.filterSectionRow = FilterSectionRow.Game
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 sessions = Filter.queryOn<Session>(realm, Query(queryCondition))
@ -225,8 +222,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.Location
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Location))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -257,11 +253,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.Location
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))
@ -287,8 +281,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentName))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -318,10 +311,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TournamentName))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -354,12 +345,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AllTournamentFeature()
val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
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))
@ -389,14 +378,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) }
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
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))
@ -423,8 +409,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentFeature))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -448,10 +433,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTableSize()
val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) }
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
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))
@ -474,9 +458,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow))
@ -499,9 +482,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filterElementRow = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -524,14 +506,12 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterElementRow.filterSectionRow = FilterSectionRow.Value
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterElementRow2.filterSectionRow = FilterSectionRow.Value
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2)))
val filterElementRow2 = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess))
@ -556,11 +536,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s3 = Session.testInstance(netResult = 500.0)
s3.cgBigBlind = 2.0
s3.cgBlinds = "2.0"
s3.result!!.buyin = 1000.0
val s4 = Session.testInstance(netResult = 570.0)
s4.cgBigBlind = 5.0
s4.cgBlinds = "5.0"
s4.result!!.buyin = 200.0
realm.commitTransaction()
@ -604,7 +584,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
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)

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

@ -1,178 +1,262 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.pokeranalytics.android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:name=".PokerAnalyticsApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/PokerAnalyticsTheme">
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:launchMode="singleTop"
android:label="@string/app_name"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:screenOrientation="portrait"
android:launchMode="singleTop">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="text/comma-separated-values" />
<data android:mimeType="text/csv" />
</intent-filter>
</activity>
<activity
android:name="net.pokeranalytics.android.ui.activity.SessionActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustNothing" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.activity.NewDataMenuActivity"
android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.EditableDataActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.FilterDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<application
android:name=".PokerAnalyticsApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:largeHeap="true"
android:theme="@style/PokerAnalyticsTheme">
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="text/comma-separated-values" />
<data android:mimeType="text/csv" />
</intent-filter>
</activity>
<!-- DatabaseCopyActivity is only used in development for now -->
<!-- <activity android:name=".ui.activity.DatabaseCopyActivity"-->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <data android:scheme="content" />-->
<!-- <data android:scheme="file" />-->
<!-- <data android:mimeType="*/*" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
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 -->
<activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog"
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"/>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

@ -1,29 +1,47 @@
package net.pokeranalytics.android
import android.app.Application
import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import io.fabric.sdk.android.Fabric
import android.content.Context
import android.os.Build
import com.google.firebase.FirebaseApp
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber
import java.util.*
class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object {
fun timeSinceInstall(context: Context): Long {
val installTime: Long = context.packageManager.getPackageInfo(context.packageName, 0).firstInstallTime
return System.currentTimeMillis() - installTime
}
}
override fun onCreate() {
super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
UserDefaults.init(this)
// AppGuard / Billing services
@ -33,37 +51,49 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(7)
.schemaVersion(14)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build()
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) {
val locales = resources.configuration.locales
CrashLogging.log("App onCreate. Locales = $locales")
}
if (BuildConfig.DEBUG) {
// Logs
Timber.plant(PokerAnalyticsLogs())
}
Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
this.createFakeSessions()
Timber.d("Realm path = ${Realm.getDefaultInstance().path}")
// 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()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
}
/**
@ -76,8 +106,8 @@ class PokerAnalyticsApplication : Application() {
realm.close()
if (sessionsCount < 10) {
GlobalScope.launch {
FakeDataManager.createFakeSessions(200)
CoroutineScope(context = Dispatchers.IO).launch {
FakeDataManager.createFakeSessions(500)
}
}

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

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

@ -1,80 +1,60 @@
package net.pokeranalytics.android.api
import android.content.Context
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.retrofit.CurrencyConverterValue
import net.pokeranalytics.android.util.URL
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
/**
* CurrencyCode Converter API
*/
interface CurrencyConverterApi {
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
companion object {
@Keep
@Serializable
data class RateResponse(var info: RateInfo)
private var currencyConverterApi: CurrencyConverterApi? = null
@Keep
@Serializable
data class RateInfo(var rate: Double)
fun getApi(context: Context): CurrencyConverterApi? {
class CurrencyConverterApi {
if (currencyConverterApi == null) {
companion object {
var serviceEndpoint = URL.API_CURRENCY_CONVERTER
val json = Json { ignoreUnknownKeys = true }
val httpClient = OkHttpClient.Builder()
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
// Logging interceptor
if (BuildConfig.DEBUG) {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BASIC
httpClient.addInterceptor(interceptor)
}
val queue = Volley.newRequestQueue(context)
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
// Add headers
val interceptor = Interceptor { chain ->
val original = chain.request()
val originalHttpUrl = original.url()
Timber.d("Api call = $url")
val url = originalHttpUrl.newBuilder()
.addQueryParameter("apiKey", context.getString(R.string.currency_converter_api))
.build()
val stringRequest = object : StringRequest(
Method.GET, url,
{ response ->
val requestBuilder = original.newBuilder()
.url(url)
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)
}) {
chain.proceed(requestBuilder.build())
override fun getHeaders(): MutableMap<String, String> {
val headers = HashMap<String, String>()
headers["apikey"] = "XnfeyID3PMKd3k4zTPW0XmZAbcZlZgqH"
return headers
}
httpClient.addInterceptor(interceptor)
val client = httpClient
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(serviceEndpoint.value)
.client(client)
.build()
currencyConverterApi = retrofit.create(CurrencyConverterApi::class.java)
}
queue.add(stringRequest)
return currencyConverterApi
}
}
@GET("convert")
fun convert(@Query("q") currencies: String, @Query("compact") compact: String = "y"): Call<Map<String, CurrencyConverterValue>>
}
}

@ -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.model.Criteria
import net.pokeranalytics.android.ui.graph.AxisFormatting
import net.pokeranalytics.android.ui.graph.Graph
enum class AggregationType {
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>
get() {
return when (this) {

@ -2,7 +2,6 @@ package net.pokeranalytics.android.calculus
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Criteria
@ -10,17 +9,13 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.realm.ComputableResult
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.model.realm.*
import net.pokeranalytics.android.util.extensions.startOfDay
import java.util.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
/**
* The class performing statIds computation
@ -31,18 +26,18 @@ class Calculator {
* The options used for calculations and display
*/
class Options(
var display: Display = Display.TABLE,
progressValues: ProgressValues = ProgressValues.NONE,
var progressValues: ProgressValues = ProgressValues.NONE,
var stats: List<Stat> = listOf(),
var criterias: List<Criteria> = listOf(),
var query: Query = Query(),
var filterId: String? = null,
private var aggregationType: AggregationType? = null,
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,
stats: List<Stat> = listOf(),
criterias: List<Criteria> = listOf(),
@ -50,18 +45,18 @@ class Calculator {
aggregationType: AggregationType? = null,
userGenerated: Boolean = false,
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
*/
var progressValues: ProgressValues = progressValues
get() {
if (field == ProgressValues.NONE && this.display == Display.PROGRESS) {
return ProgressValues.STANDARD
}
return field
}
// var progressValues: ProgressValues = progressValues
// get() {
// if (field == ProgressValues.NONE && this.display.requireProgressValues) {
// return ProgressValues.STANDARD
// }
// return field
// }
init {
this.aggregationType?.let {
@ -69,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
@ -119,7 +80,7 @@ class Calculator {
val computeStandardDeviation: Boolean
get() {
this.stats.forEach {
if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) {
if (it.isStandardDeviation) {
return true
}
}
@ -133,6 +94,7 @@ class Calculator {
get() {
return this.stats.contains(LONGEST_STREAKS)
}
/**
* Whether the values should be sorted
*/
@ -140,6 +102,7 @@ class Calculator {
get() {
return this.progressValues != ProgressValues.NONE || this.computeLongestStreak
}
/**
* Whether the number of locations played should be computed
*/
@ -189,7 +152,6 @@ class Calculator {
): Report {
val options = Options(
display = Options.Display.PROGRESS,
progressValues = Options.ProgressValues.STANDARD,
stats = listOf(stat),
aggregationType = aggregationType
@ -215,13 +177,13 @@ class Calculator {
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)
val group = ComputableGroup(comparatorQuery)
computableGroups.add(group)
}
if (computableGroups.size == 0) {
@ -239,7 +201,6 @@ class Calculator {
val report = Report(options)
groups.forEach { group ->
// val s = Date()
// Clean existing computables / sessionSets if group is reused
group.cleanup()
@ -279,11 +240,27 @@ class Calculator {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val computables = computableGroup.computables(realm, options.shouldSortValues)
// Timber.d(">>>> Start computing group ${computableGroup.name}, ${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())
// computables.forEach {
// Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}")
// }
var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
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
}
}
val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
results.addStat(NET_RESULT, sum)
results.addStat(NET_RESULT, ratedNet)
val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
results.addStat(HANDS_PLAYED, totalHands)
@ -300,41 +277,60 @@ class Calculator {
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(TOTAL_BUYIN, totalBuyin)
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()
maxNetResult?.let {
results.addStat(MAXIMUM_NETRESULT, it)
results.addStat(MAXIMUM_NET_RESULT, it)
}
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let {
results.addStat(MINIMUM_NETRESULT, it)
results.addStat(MINIMUM_NET_RESULT, it)
}
Stat.netBBPer100Hands(bbSum, totalHands)?.let { 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)
}
// 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) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
}
var average = 0.0 // also used for standard deviation later
if (computables.size > 0) {
average = sum / computables.size.toDouble()
average = ratedNet / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats(
setOf(
ComputedStat(AVERAGE, average),
ComputedStat(WIN_RATIO, winRatio),
ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio),
ComputedStat(AVERAGE_BUYIN, avgBuyin)
)
)
}
var averageBB = 0.0
if (bbSessionCount > 0) {
averageBB = bbSum / bbSessionCount
results.addStat(AVERAGE_NET_BB, averageBB)
}
val shouldIterateOverComputables =
(options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak)
@ -352,6 +348,7 @@ class Calculator {
var longestWinStreak = 0
var longestLoseStreak = 0
var currentStreak = 0
var tITMCount = 0
computables.forEach { computable ->
index++
@ -359,6 +356,7 @@ class Calculator {
tBBSum += computable.bbNet
tBBSessionCount += computable.hasBigBlind
tWinningSessionCount += computable.isPositive
tITMCount += computable.isPositive
tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands
@ -384,11 +382,16 @@ class Calculator {
results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session)
results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session)
results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
results.addEvolutionValue(tBBSum, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO,
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(computable.ratedNet, stat = STANDARD_DEVIATION, data = session)
@ -430,9 +433,9 @@ class Calculator {
}
}
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() ||
options.progressValues != Options.ProgressValues.NONE ||
options.computeDaysPlayed
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty()
|| options.progressValues != Options.ProgressValues.NONE
|| options.computeDaysPlayed
// Session Set
if (shouldIterateOverSets) {
@ -479,10 +482,6 @@ class Calculator {
)
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 -> {
results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet)
@ -534,7 +533,7 @@ class Calculator {
var hourlyRate = 0.0
if (gHourlyDuration != null) {
hourlyRate = sum / gHourlyDuration
hourlyRate = ratedNet / gHourlyDuration
if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate)
@ -561,15 +560,19 @@ class Calculator {
// Session
var stdSum = 0.0
var stdBBSum = 0.0
var stdBBper100HandsSum = 0.0
computables.forEach { session ->
stdSum += Math.pow(session.ratedNet - average, 2.0)
stdBBper100HandsSum += Math.pow(session.bbPer100Hands - bbPer100Hands, 2.0)
stdSum += (session.ratedNet - average).pow(2.0)
stdBBSum += (session.bbNet - averageBB).pow(2.0)
stdBBper100HandsSum += (session.bbPer100Hands - bbPer100Hands).pow(2.0)
}
val standardDeviation = Math.sqrt(stdSum / computables.size)
val standardDeviationBBper100Hands = Math.sqrt(stdBBper100HandsSum / computables.size)
val standardDeviation = sqrt(stdSum / computables.size)
val standardDeviationBB = sqrt(stdBBSum / computables.size)
val standardDeviationBBper100Hands = sqrt(stdBBper100HandsSum / computables.size)
results.addStat(STANDARD_DEVIATION, standardDeviation)
results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB)
results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands)
// Session Set
@ -578,9 +581,9 @@ class Calculator {
sessionSets.forEach { set ->
val ssStats = SSStats(set, computableGroup.query)
val sHourlyRate = ssStats.hourlyRate
hourlyStdSum += Math.pow(sHourlyRate - hourlyRate, 2.0)
hourlyStdSum += (sHourlyRate - hourlyRate).pow(2.0)
}
val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size)
val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size)
results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
}
@ -616,10 +619,10 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet)
} else {
ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumByDouble { it.bbNet }
ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet }
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
}
}

@ -0,0 +1,35 @@
package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.util.TextFormat
import java.util.*
/**
* ComputedStat contains a [stat] and their associated [value]
*/
class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = null, var currency: Currency? = null) {
constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
if (previousValue != null) {
this.variation = (value - previousValue) / previousValue
}
}
/**
* The value used to get evolution dataset
*/
var progressValue: Double? = null
/**
* The variation of the stat
*/
var variation: Double? = null
/**
* Formats the value of the stat to be suitable for display
*/
val textFormat: TextFormat
get() {
return this.stat.textFormat(this.value, this.secondValue, this.currency)
}
}

@ -1,26 +1,13 @@
package net.pokeranalytics.android.calculus
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.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry
//import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.model.realm.ComputableResult
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.Graph
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import net.pokeranalytics.android.util.TextFormat
import kotlin.math.abs
/**
* The class returned after performing calculation in the Calculator object
@ -32,7 +19,10 @@ class Report(var options: Calculator.Options) {
*/
private var _results: MutableList<ComputedResults> = mutableListOf()
val results: List<ComputedResults> = this._results
val results: List<ComputedResults>
get() {
return this._results
}
/**
* Adds a new result to the list of ComputedResults
@ -41,142 +31,32 @@ class Report(var options: Calculator.Options) {
this._results.add(result)
}
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: 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))
fun max(stat: Stat): ComputedResults? {
var computedResults: ComputedResults? = null
var count = 0
var max = Double.MIN_VALUE
for (cr in this._results) {
cr.computedStat(stat)?.value?.let { value ->
count += 1
if (value > max) {
computedResults = cr
max = value
}
}
}
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)
}
}
}
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
return if (count >= 2) { computedResults } else { null }
}
}
/**
* 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, shouldManageMultiGroupProgressValues: Boolean = false) :
GraphUnderlyingEntry {
class ComputedResults(group: ComputableGroup,
shouldManageMultiGroupProgressValues: Boolean = false) : GraphUnderlyingEntry {
/**
* The session group used to computed the statIds
@ -191,9 +71,13 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues
fun allStats(): Collection<ComputedStat> {
return this._computedStats.values
}
private val allStats: Collection<ComputedStat>
get() { return this._computedStats.values }
val evolutionValues: Map<Stat, MutableList<Point>>
get() {
return this._evolutionValues
}
/**
* Adds a value to the evolution values
@ -243,7 +127,7 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
fun computeProgressValues(computedResults: ComputedResults) {
this.allStats().forEach { computedStat ->
this.allStats.forEach { computedStat ->
val stat = computedStat.stat
computedResults.computedStat(stat)?.let { previousComputedStat ->
when (stat) {
@ -278,7 +162,7 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
val bbSessionCount = this.computedStat(Stat.BB_SESSION_COUNT)?.progressValue
val totalBuyin = this.computedStat(Stat.TOTAL_BUYIN)?.progressValue
this.allStats().forEach { computedStat ->
this.allStats.forEach { computedStat ->
when (computedStat.stat) {
Stat.HOURLY_RATE -> {
if (netResult != null && duration != null) {
@ -353,79 +237,6 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
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
get() {
return this.group.isEmpty
@ -439,7 +250,7 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
override fun formattedValue(stat: Stat): TextFormat {
this.computedStat(stat)?.let {
return it.format()
return it.textFormat
} ?: run {
throw PAIllegalStateException("Missing stat in results")
}
@ -447,23 +258,23 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
override fun legendValues(
stat: Stat,
entry: Entry,
style: GraphFragment.Style,
total: Double,
style: Graph.Style,
groupName: String,
context: Context
): LegendContent {
when (style) {
GraphFragment.Style.BAR -> {
Graph.Style.BAR -> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.format()
val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.textFormat
DefaultLegendValues(this.entryTitle(context), entryValue, countValue)
}
}
@ -472,12 +283,12 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue)
}
}
@ -486,10 +297,4 @@ class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValu
}
}
}
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.exceptions.FormattingException
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.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
@ -17,14 +18,12 @@ import java.util.*
import kotlin.math.exp
import kotlin.math.pow
class StatFormattingException(message: String) : Exception(message) {
}
class StatFormattingException(message: String) : Exception(message)
/**
* 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),
BB_NET_RESULT(2),
@ -46,14 +45,17 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED(18),
LOCATIONS_PLAYED(19),
LONGEST_STREAKS(20),
MAXIMUM_NETRESULT(21),
MINIMUM_NETRESULT(22),
MAXIMUM_NET_RESULT(21),
MINIMUM_NET_RESULT(22),
MAXIMUM_DURATION(23),
DAYS_PLAYED(24),
WINNING_SESSION_COUNT(25),
BB_SESSION_COUNT(26),
TOTAL_BUYIN(27),
RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31)
;
companion object : IntSearchable<Stat> {
@ -86,7 +88,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return netBB / numberOfHands * 100
}
fun riskOfRuin(hourlyRate: Double, hourlyStandardDeviation: Double, bankrollValue: Double) : Double? {
fun riskOfRuin(hourlyRate: Double, hourlyStandardDeviation: Double, bankrollValue: Double): Double? {
if (bankrollValue <= 0.0) {
return null
@ -101,6 +103,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
}
override val value: Int = this.uniqueIdentifier
override val resId: Int?
get() {
return when (this) {
@ -121,14 +125,17 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
STANDARD_DEVIATION -> R.string.standard_deviation
STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation
HANDS_PLAYED -> R.string.number_of_hands
LOCATIONS_PLAYED -> R.string.locations_played
LONGEST_STREAKS -> R.string.longest_streaks
MAXIMUM_NETRESULT -> R.string.max_net_result
MINIMUM_NETRESULT -> R.string.min_net_result
MAXIMUM_NET_RESULT -> R.string.max_net_result
MINIMUM_NET_RESULT -> R.string.min_net_result
MAXIMUM_DURATION -> R.string.longest_session
DAYS_PLAYED -> R.string.days_played
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")
}
}
@ -137,7 +144,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
/**
* Formats the value of the stat to be suitable for display
*/
fun format(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat {
fun textFormat(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat {
if (value.isNaN()) {
return TextFormat(NULL_TEXT, R.color.white)
@ -145,14 +152,14 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
when (this) {
// 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
return TextFormat(value.toCurrency(currency), color)
}
// Red/green numericValues
HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS, BB_NET_RESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.formatted(), color)
return TextFormat(value.formatted, color)
}
// white integers
NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> {
@ -161,17 +168,17 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
return TextFormat(value.formattedHourlyDuration())
} // 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
return TextFormat("${(value * 100).formatted()}%", color)
return TextFormat("${(value * 100).formatted}%", color)
}
RISK_OF_RUIN -> {
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,
STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_BUYIN -> {
STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN, TOTAL_TIPS -> {
return TextFormat(value.toCurrency(currency))
}
LONGEST_STREAKS -> {
@ -185,6 +192,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
get() {
return when (this) {
RISK_OF_RUIN -> 5.0
TOURNAMENT_ITM_RATIO -> 10.0
WIN_RATIO -> 50.0
else -> 0.0
}
@ -200,11 +208,12 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records
NET_RESULT -> R.string.total
NET_RESULT, BB_NET_RESULT -> R.string.total
STANDARD_DEVIATION -> R.string.net_result
STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
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
}
resId?.let {
@ -237,19 +246,40 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val hasProgressGraph: Boolean
get() {
return when (this) {
HOURLY_DURATION, AVERAGE_HOURLY_DURATION,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> false
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, HANDS_PLAYED,
STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_TIPS -> false
else -> true
}
}
val isStandardDeviation: Boolean
get() {
return when (this) {
STANDARD_DEVIATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
else -> false
}
}
val legendHideRightValue: Boolean
get() {
return when (this) {
AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, WIN_RATIO, TOURNAMENT_ITM_RATIO,
HOURLY_DURATION, AVERAGE_HOURLY_DURATION -> true
else -> false
}
}
/**
* Returns if the stat has a significant value to display in a progress graph
*/
val graphSignificantIndividualValue: Boolean
get() {
return when (this) {
WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES, STANDARD_DEVIATION, HOURLY_DURATION -> false
AVERAGE, WIN_RATIO, TOURNAMENT_ITM_RATIO,
NUMBER_OF_SETS, NUMBER_OF_GAMES,
STANDARD_DEVIATION, HOURLY_DURATION -> false
else -> true
}
}
@ -294,7 +324,8 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return when (this) {
NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
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
else -> false
}
@ -302,33 +333,3 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}
/**
* ComputedStat contains a [stat] and their associated [value]
*/
class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = null, var currency: Currency? = null) {
constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
if (previousValue != null) {
this.variation = (value - previousValue) / previousValue
}
}
/**
* The value used to get evolution dataset
*/
var progressValue: Double? = null
/**
* The variation of the stat
*/
var variation: Double? = null
/**
* Formats the value of the stat to be suitable for display
*/
fun format(): TextFormat {
return this.stat.format(this.value, this.secondValue, this.currency)
}
}

@ -6,6 +6,8 @@ import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
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.realm.*
import net.pokeranalytics.android.util.extensions.findById
@ -15,10 +17,9 @@ class BankrollCalculator {
fun computeReport(realm: Realm, setup: BankrollReportSetup) : BankrollReport {
//val realm = Realm.getDefaultInstance()
val report = BankrollReport(setup)
realm.refresh() // fixes an issue where a newly created bankroll is not found, throwing an exception
val bankrolls: List<Bankroll> =
if (setup.bankrollId != null) {
val bankroll = realm.findById<Bankroll>(setup.bankrollId) ?: throw PAIllegalStateException("Bankroll not found with id=${setup.bankrollId}")
@ -38,30 +39,49 @@ class BankrollCalculator {
initialValue += bankroll.initialValue * rate
}
bankroll.transactions?.let { transactions ->
val sum = transactions.sum("amount")
if (setup.virtualBankroll) {
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()
} 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.initial = initialValue
val query = setup.query(realm)
val transactions = Filter.queryOn<Transaction>(realm, query)
val baseQuery = setup.query(realm)
val transactions = Filter.queryOn<Transaction>(realm, baseQuery)
report.addDatedItems(transactions)
transactions.forEach {
report.addTransaction(it)
}
val sessions = Filter.queryOn<Session>(realm, query)
val sessionQuery = Query(QueryCondition.DateNotNull).merge(baseQuery)
val sessions = Filter.queryOn<Session>(realm, sessionQuery)
report.addDatedItems(sessions)
if (setup.virtualBankroll) {
val options = Calculator.Options(stats = listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY))
val group = ComputableGroup(query)
val group = ComputableGroup(baseQuery)
val result = Calculator.compute(realm, group, options)
result.computedStat(Stat.NET_RESULT)?.let {
report.netResult = it.value
@ -69,20 +89,18 @@ class BankrollCalculator {
this.computeRiskOfRuin(report, result)
} else {
val results = Filter.queryOn<Result>(realm, query)
val results = Filter.queryOn<Result>(realm, baseQuery)
report.netResult = results.sum("net").toDouble()
}
val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
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)
report.transactionBuckets[withdrawalType.id]?.let { bucket ->
report.withdrawalTotal = bucket.transactions.sumByDouble { it.amount }
report.withdrawalTotal = bucket.transactions.sumOf { it.amount }
}
report.generateGraphPointsIfNecessary()

@ -13,7 +13,6 @@ import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
import kotlin.collections.HashMap
/**
* This class holds the results from the BankrollCalculator computations
@ -64,6 +63,7 @@ class BankrollReport(var setup: BankrollReportSetup) {
*/
private fun computeBankrollTotal() {
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)
}
this.from?.let {
val fromCondition = QueryCondition.StartedFromDate()
fromCondition.singleValue = it
query.add(fromCondition)
query.add(QueryCondition.StartedFromDate(it))
}
this.to?.let {
val toCondition = QueryCondition.StartedToDate()
toCondition.singleValue = it
query.add(toCondition)
query.add(QueryCondition.StartedToDate(it))
}
return query
}

@ -15,7 +15,7 @@ import kotlin.coroutines.CoroutineContext
object BankrollReportManager {
val coroutineContext: CoroutineContext
private val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var reports: MutableMap<String?, BankrollReport> = mutableMapOf()
@ -27,7 +27,6 @@ object BankrollReportManager {
init {
val realm = Realm.getDefaultInstance()
realm.isAutoRefresh = true
computableResults = realm.where(ComputableResult::class.java).findAll()
bankrolls = realm.where(Bankroll::class.java).findAll()
transactions = realm.where(Transaction::class.java).findAll()
@ -60,6 +59,7 @@ object BankrollReportManager {
fun reportForBankroll(bankrollId: String?, handler: (BankrollReport) -> Unit) {
Timber.d("Request bankroll report for bankrollId = $bankrollId")
// if the report exists, return it
val existingReport: BankrollReport? = this.reports[bankrollId]
if (existingReport != null) {

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

@ -0,0 +1,183 @@
package net.pokeranalytics.android.calculus.optimalduration
import io.realm.Realm
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Session
import org.apache.commons.math3.fitting.PolynomialCurveFitter
import org.apache.commons.math3.fitting.WeightedObservedPoints
import timber.log.Timber
import java.util.*
import kotlin.math.pow
import kotlin.math.round
/***
* This class attempts to find the optimal game duration,
* meaning the duration where the player will maximize its results, based on his history.
* The results stands for cash game, and are separated between live and online.
* Various reasons can prevent the algorithm to find a duration, see below.
*/
class CashGameOptimalDurationCalculator {
companion object {
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 minimumValidityCount =
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
private const val polynomialDegree = 7 // the degree of the computed polynomial
/***
* Starts the calculation
* [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
*/
fun start(isLive: Boolean): Double? {
val realm = Realm.getDefaultInstance()
val query = Query().add(QueryCondition.IsCash) // cash game
query.add(
if (isLive) {
QueryCondition.IsLive
} else {
QueryCondition.IsOnline
}
) // live / online
query.add(QueryCondition.EndDateNotNull) // ended
query.add(QueryCondition.BiggestBetNotNull) // has BB value
val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
val sessionsByDuration = sessions.groupBy {
val dur = round((it.netDuration / bucket).toDouble()) * bucket
// Timber.d("Stop notif > key: $dur")
dur
}
// define validity interval
var start: Double? = null
var end: Double? = null
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
// Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
start = key
}
if (sessionCount >= minimumValidityCount) {
end = key
validBuckets++
}
}
// Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
// Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
// Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
val options = Calculator.Options()
options.query = query
val report = Calculator.computeStats(realm, options)
val stdBB =
report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
val p = polynomialRegression(sessions, stdBB)
var bestAverage = 0.0
var bestHourlyRate = 0.0
var bestDuration = 0.0
var maxDuration = 0.0
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
val averageResult = getBB(duration, p)
val hourly = averageResult / duration
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) {
bestAverage = averageResult
bestDuration = duration
}
if (duration > 0 && hourly > bestHourlyRate) {
bestHourlyRate = hourly
}
if (duration > maxDuration) {
maxDuration = duration
}
}
}
if (bestDuration > 0.0) {
return bestDuration
}
// Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
var y = 0.0
for (i in polynomial.indices) {
y += polynomial[i] * netDuration.pow(i)
}
return y
}
private fun polynomialRegression(
sessions: List<Session>,
bbStandardDeviation: Double?
): DoubleArray {
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
val points = WeightedObservedPoints()
val now = Date().time
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
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
}
}
}

@ -1,6 +1,6 @@
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 FormattingException(message: String) : Exception(message)
@ -14,15 +14,19 @@ class PAIllegalStateException(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 FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition")
object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized")
object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing")
object QueryTypeUnhandled: PokerAnalyticsException(message = "queryWith type not handled")
// 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 FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
class QueryValueMapUnknown(message: String = "fieldName is missing"): PokerAnalyticsException(message)
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 FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null")
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 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 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 UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow")
// data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
// 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.PokerAnalyticsException
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.Limits.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.TournamentFeatures.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 {
objects.add(QueryCondition.CustomFieldListQuery(it))
}
objects.sorted()
objects.sort()
realm.close()
return objects.map { Query(it) }
}
@ -104,11 +104,10 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
}
objects.add(condition)
}
objects.sorted()
objects.sort()
return objects.map { Query(it) }
}
QueryCondition.distinct<Session, T, S>()?.let {
val values = it.mapNotNull { session ->
when (this) {
@ -124,8 +123,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is TournamentFees -> if (session.tournamentEntryFee is S) {
session.tournamentEntryFee as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is Blinds -> if (session.blinds is S) {
session.blinds as S
is Stakes -> if (session.cgStakes is S) {
session.cgStakes as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
else -> null
}
@ -159,20 +158,21 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14)
object Years : ListCriteria(15)
object AllMonthsUpToNow : ListCriteria(16)
object Blinds : ListCriteria(17)
object Stakes : ListCriteria(17)
object TournamentFees : ListCriteria(18)
object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
val queries: List<Query>
get() {
return when (this) {
is AllMonthsUpToNow -> {
val realm = Realm.getDefaultInstance()
val firstSession = realm.where<Session>().sort("startDate", Sort.ASCENDING).findFirst()
val lastSession = realm.where<Session>().sort("startDate", Sort.DESCENDING).findFirst()
val firstSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.ASCENDING).findFirst()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst()
realm.close()
val years: ArrayList<Query> = arrayListOf()
@ -223,10 +223,10 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is Years -> {
val years = arrayListOf<Query>()
val realm = Realm.getDefaultInstance()
val lastSession = realm.where<Session>().sort("startDate", Sort.DESCENDING).findFirst()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst()
val yearNow = lastSession?.year ?: return years
realm.where<Session>().sort("year", Sort.ASCENDING).findFirst()?.year?.let {
realm.where<Session>().isNotNull("startDate").sort("year", Sort.ASCENDING).findFirst()?.year?.let {
for (index in 0..(yearNow - it)) {
val yearCondition = QueryCondition.AnyYear().apply {
listOfValues = arrayListOf(it + index)
@ -237,19 +237,34 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.close()
years
}
is Blinds -> comparison<QueryCondition.AnyBlind, String>()
is Stakes -> comparison<QueryCondition.AnyStake, String>()
is ListCustomFields -> comparison<CustomFieldEntry>()
is ValueCustomFields -> {
val realm = Realm.getDefaultInstance()
val queries = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
else -> throw PokerAnalyticsException.QueryTypeUnhandled
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
}
realm.close()
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
Years -> R.string.year
AllMonthsUpToNow -> R.string.month
Blinds -> R.string.blind
Stakes -> R.string.blind
TournamentFees -> R.string.entry_fees
// is ListCustomFields -> 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> {
val objects = mutableListOf<S>()
val realm = Realm.getDefaultInstance()
realm.where<T>().findAll().forEach {
realm.where<T>().sort("name").findAll().forEach {
val condition = (QueryCondition.getInstance<T>() as S).apply {
setObject(it)
}
objects.add(condition)
}
objects.sorted()
// objects.sort()
realm.close()
return objects.map { Query(it) }
}
@ -303,7 +318,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
}
objects.add(condition)
}
objects.sorted()
objects.sort()
return objects.map { Query(it) }
}
@ -319,7 +334,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
TournamentFeatures, Limits, TableSizes, TournamentTypes,
MonthsOfYear, DaysOfWeek, SessionTypes,
BankrollTypes, DayPeriods, Years,
AllMonthsUpToNow, Blinds, TournamentFees
AllMonthsUpToNow,
Stakes, TournamentFees
)
}
}

@ -14,11 +14,11 @@ enum class Limit : RowRepresentable {
fun getInstance(value: String) : Limit? {
return when (value) {
"No Limit" -> NO
"Pot Limit" -> POT
"Fixed Limit", "Limit" -> FIXED
"Mixed Limit" -> MIXED
"Spread Limit" -> SPREAD
"NL", "No Limit" -> NO
"PL", "Pot Limit" -> POT
"FL", "Fixed Limit", "Limit" -> FIXED
"ML", "Mixed Limit" -> MIXED
"SL", "Spread Limit" -> SPREAD
else -> null
}
}
@ -38,13 +38,12 @@ enum class Limit : RowRepresentable {
val longName: String
get() {
val limit = "Limit"
return when (this) {
NO -> "No $limit"
POT -> "Pot $limit"
FIXED -> "Fixed $limit"
MIXED -> "Mixed $limit"
SPREAD -> "Spread $limit"
NO -> "No Limit"
POT -> "Pot Limit"
FIXED -> "Limit"
MIXED -> "Mixed Limit"
SPREAD -> "Spread Limit"
}
}

@ -1,10 +1,15 @@
package net.pokeranalytics.android.model
import android.content.Context
import androidx.fragment.app.Fragment
import io.realm.Realm
import io.realm.Sort
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.Localizable
import net.pokeranalytics.android.util.extensions.findById
@ -21,7 +26,9 @@ enum class LiveData : Localizable {
TRANSACTION_TYPE,
FILTER,
CUSTOM_FIELD,
REPORT_SETUP;
REPORT_SETUP,
PLAYER,
HAND_HISTORY;
var subType:Int? = null
@ -38,6 +45,8 @@ enum class LiveData : Localizable {
FILTER -> Filter::class.java
CUSTOM_FIELD -> CustomField::class.java
REPORT_SETUP -> ReportSetup::class.java
PLAYER -> Player::class.java
HAND_HISTORY -> HandHistory::class.java
}
}
@ -78,6 +87,8 @@ enum class LiveData : Localizable {
FILTER -> R.string.filter
CUSTOM_FIELD -> R.string.custom_field
REPORT_SETUP -> R.string.custom
PLAYER -> R.string.player
HAND_HISTORY -> R.string.hand_history
}
}
@ -94,6 +105,8 @@ enum class LiveData : Localizable {
FILTER -> R.string.filters
CUSTOM_FIELD -> R.string.custom_fields
REPORT_SETUP -> R.string.custom
PLAYER -> R.string.players
HAND_HISTORY -> R.string.hands_history
}
}
@ -110,10 +123,18 @@ enum class LiveData : Localizable {
FILTER -> R.string.new_filter
CUSTOM_FIELD -> R.string.new_custom_field
REPORT_SETUP -> R.string.new_report
PLAYER -> R.string.new_friend
HAND_HISTORY -> R.string.new_hand
}
}
val isSearchable: Boolean
get() {
return when (this) {
PLAYER, LOCATION -> true
else -> false
}
}
/**
* Return the new entity titleResId
@ -136,4 +157,33 @@ enum class LiveData : Localizable {
return context.getString(this.pluralResId, context)
}
fun openEditActivity(fragment: Fragment, primaryKey: String? = null, requestCode: Int) {
when (this) {
HAND_HISTORY -> {
HandHistoryActivity.newInstance(fragment, primaryKey)
}
else -> {
EditableDataActivity.newInstanceForResult(fragment, this, primaryKey, requestCode)
}
}
}
val sortFields: Array<String>
get() {
return when (this) {
TRANSACTION, HAND_HISTORY -> arrayOf("date")
FILTER -> arrayOf("useCount", "name")
else -> arrayOf("name")
}
}
val sortOrders: Array<Sort>
get() {
return when (this) {
TRANSACTION, HAND_HISTORY -> arrayOf(Sort.DESCENDING)
FILTER -> arrayOf(Sort.DESCENDING, Sort.ASCENDING)
else -> arrayOf(Sort.ASCENDING)
}
}
}

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

@ -4,18 +4,28 @@ import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.Parser
class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITLE_GRID.ordinal) : RowRepresentable {
class TableSize(
var numberOfPlayer: Int,
var rowViewType: Int = RowViewType.TITLE_GRID.ordinal,
var alternativeLabels: Boolean = false
) : RowRepresentable {
companion object {
val all: List<TableSize>
get() {
return Array(9, init =
{ index -> TableSize(index + 2) }).toList()
}
fun all(alternativeLabels: Boolean): List<TableSize> {
return Array(9, init = { index ->
TableSize(index + 2, alternativeLabels = alternativeLabels)
}).toList()
}
fun valueForLabel(label: String) : Int? {
Parser.parseNumber(label)?.let {
return it.toInt()
}
return when (label) {
"Full Ring", "Full-Ring" -> 10
"Short-Handed", "Short Handed" -> 6
@ -26,6 +36,10 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL
}
override fun getDisplayName(context: Context): String {
if (this.alternativeLabels) {
return this.numberOfPlayer.toString()
}
return if (this.numberOfPlayer == 2) {
return "HU"
} else {
@ -43,6 +57,10 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL
}
override fun localizedTitle(context: Context): String {
if (this.alternativeLabels) {
return this.numberOfPlayer.toString()
}
this.resId?.let {
return if (this.numberOfPlayer == 2) {
context.getString(it)

@ -5,20 +5,20 @@ import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
enum class TournamentType : RowRepresentable {
MTT,
SNG;
enum class TournamentType(val label: String) : RowRepresentable {
MTT("MTT"),
SNG("SNG");
companion object {
val all : List<TournamentType>
get() {
return TournamentType.values() as List<TournamentType>
return values().toList()
}
fun getValueForLabel(label: String) : TournamentType? {
return when (label) {
"Single-Table" -> SNG
"Multi-Table" -> MTT
SNG.label, "Single-Table" -> SNG
MTT.label, "Multi-Table" -> MTT
else -> null
}
}

@ -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 = ""
}

@ -1,11 +1,17 @@
package net.pokeranalytics.android.model.extensions
import android.content.Context
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.NotificationSchedule
import net.pokeranalytics.android.util.extensions.toCurrency
import java.util.*
import java.util.concurrent.TimeUnit
enum class SessionState {
PENDING,
@ -14,7 +20,7 @@ enum class SessionState {
PAUSED,
FINISHED;
var hasStarted: Boolean = false
val hasStarted: Boolean
get() {
return when (this) {
STARTED, PAUSED, FINISHED -> true
@ -30,27 +36,27 @@ enum class SessionState {
fun Session.getState(): SessionState {
val start = this.startDate
if (start == null) {
return SessionState.PENDING
return if (start == null) {
SessionState.PENDING
} else {
if (start > Date()) {
return SessionState.PLANNED
SessionState.PLANNED
} else if (this.endDate != null) {
return SessionState.FINISHED
SessionState.FINISHED
} else if (this.pauseDate != null) {
return SessionState.PAUSED
SessionState.PAUSED
} else {
return SessionState.STARTED
SessionState.STARTED
}
}
}
/**
* Formate the session game type
* Format the session game type
*/
fun Session.getFormattedGameType(context: Context): String {
var parameters = mutableListOf<String>()
val parameters = mutableListOf<String>()
if (isTournament()) {
tournamentEntryFee?.let {
@ -70,8 +76,8 @@ fun Session.getFormattedGameType(context: Context): String {
parameters.add(context.getString(R.string.tournament).capitalize())
}
} else {
if (cgSmallBlind != null && cgBigBlind != null) {
parameters.add(getFormattedBlinds())
if (this.cgAnte != null || this.cgBlinds != null) {
parameters.add(getFormattedStakes())
}
game?.let {
parameters.add(getFormattedGame())
@ -84,6 +90,40 @@ fun Session.getFormattedGameType(context: Context): String {
return parameters.joinToString(separator = " ")
}
fun Session.currentDuration(): Long {
return this.startDate?.let {
this.endDate().time - it.time
} ?: 0L
}
fun Session.cancelStopNotification(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(this.id)
}
fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
val timeDelay = optimalDuration - this.currentDuration()
if (timeDelay <= 0) {
return
}
val title = context.getString(R.string.stop_notification_title)
val body = context.getString(R.string.stop_notification_body)
val data = Data.Builder()
.putString(NotificationSchedule.ParamKeys.TITLE.value, title)
.putString(NotificationSchedule.ParamKeys.BODY.value, body)
val work = OneTimeWorkRequestBuilder<NotificationSchedule>()
.setInitialDelay(timeDelay, TimeUnit.MILLISECONDS)
.setInputData(data.build())
.addTag(this.id)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
}
val AbstractList<Session>.hourlyDuration: Double
get() {
val intervals = mutableListOf<TimeInterval>()
@ -91,7 +131,7 @@ val AbstractList<Session>.hourlyDuration: Double
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval)
}
return intervals.sumByDouble { it.hourlyDuration }
return intervals.sumOf { it.hourlyDuration }
}
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {
@ -113,7 +153,7 @@ fun MutableList<TimeInterval>.update(timeInterval: TimeInterval): MutableList<Ti
}
if (overlapped.size == 0) { // add
if (overlapped.isEmpty()) { // add
this.add(timeInterval)
} else { // update

@ -2,8 +2,9 @@ package net.pokeranalytics.android.model.filter
import io.realm.RealmModel
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.CrashLogging
/**
* We want to be able to store filters in the database:
@ -57,20 +58,19 @@ class FilterHelper {
inline fun <reified T : Filterable > fieldNameForQueryType(queryCondition: Class< out QueryCondition>): String? {
val fieldName = when (T::class.java) {
return when (T::class.java) {
Session::class.java -> Session.fieldNameForQueryType(queryCondition)
ComputableResult::class.java -> ComputableResult.fieldNameForQueryType(queryCondition)
SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition)
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition)
else -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null
// throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}")
}
}
return fieldName
/*
fieldName?.let {
return fieldName

@ -11,9 +11,13 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query {
constructor(vararg elements: QueryCondition) {
if (elements.size > 0) {
this.add(elements.asList())
constructor(query: Query) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) {
this.add(elements.filterNotNull())
}
}
@ -23,22 +27,25 @@ class Query {
return this._conditions
}
fun add(vararg elements: QueryCondition) {
if (elements.size > 0) {
fun add(vararg elements: QueryCondition): Query {
if (elements.isNotEmpty()) {
this.add(elements.asList())
}
return this
}
fun add(queryCondition: QueryCondition) {
fun add(queryCondition: QueryCondition): Query {
this._conditions.add(queryCondition)
return this
}
fun remove(queryCondition: QueryCondition) {
this._conditions.remove(queryCondition)
fun add(queryConditions: List<QueryCondition>): Query {
this._conditions.addAll(queryConditions)
return this
}
fun add(queryConditions: List<QueryCondition>) {
this._conditions.addAll(queryConditions)
fun remove(queryCondition: QueryCondition) {
this._conditions.remove(queryCondition)
}
val defaultName: String
@ -49,42 +56,46 @@ class Query {
}
}
fun getName(context: Context): String {
fun getName(context: Context, separator: String = " + "): String {
return when (this._conditions.size) {
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) }
}
}
inline fun <reified T : Filterable> queryWith(query: RealmQuery<T>): RealmQuery<T> {
var realmQuery = query
val queryFromTime = this.conditions.filter {
it is QueryCondition.StartedFromTime
}.firstOrNull()
val queryToTime = this.conditions.filter {
it is QueryCondition.EndedToTime
}.firstOrNull()
this.conditions.forEach {
if (it is QueryCondition.StartedFromTime) {
realmQuery = it.queryWith(realmQuery, queryToTime)
} else if (it is QueryCondition.EndedToTime) {
realmQuery = it.queryWith(realmQuery, queryFromTime)
} else {
realmQuery = it.queryWith(realmQuery)
}
val queryFromTime = this.conditions.firstOrNull {
it is QueryCondition.StartedFromTime
}
val queryToTime = this.conditions.firstOrNull {
it is QueryCondition.EndedToTime
}
this.conditions.forEach {
realmQuery = when (it) {
is QueryCondition.StartedFromTime -> {
it.queryWith(realmQuery, queryToTime)
}
is QueryCondition.EndedToTime -> {
it.queryWith(realmQuery, queryFromTime)
}
else -> {
it.queryWith(realmQuery)
}
}
}
// println("<<<<<< ${realmQuery.description}")
val queryLast = this.conditions.filter {
it is QueryCondition.Last
}.firstOrNull()
queryLast?.let {qc ->
(qc as QueryCondition.Last).singleValue?.let {
return realmQuery.limit(it.toLong())
}
}
// val queryLast = this.conditions.firstOrNull {
// it is QueryCondition.Last
// }
// queryLast?.let {qc ->
// (qc as QueryCondition.Last).singleValue?.let {
// return realmQuery.limit(it.toLong())
// }
// }
return realmQuery
}
@ -93,4 +104,25 @@ class Query {
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.TableSize
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.NameManageable
import net.pokeranalytics.android.model.interfaces.StakesHolder
import net.pokeranalytics.android.model.realm.*
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.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.text.DateFormatSymbols
import java.text.NumberFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.reflect.KClass
/**
* Enum describing the way a query should be handled
* Some queries requires a value to be checked upon through equals, in, more, less, between
*/
sealed class QueryCondition : FilterElementRow {
sealed class QueryCondition : RowRepresentable {
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 {
return T::class.java.newInstance().apply { this.operator = Operator.LESS }
// inline fun <reified T : QueryCondition> more(): T {
// 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> {
return arrayListOf(more(), less())
inline fun <reified T : QueryCondition> lessOrEqual(): T {
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 {
val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin
val instance = kClass.objectInstance ?: kClass.java.newInstance()
return instance as T
return newInstance(kClass) as T
}
inline fun <reified T : Identifiable> getInstance(): QueryCondition {
@ -61,13 +89,14 @@ sealed class QueryCondition : FilterElementRow {
TransactionType::class.java -> AnyTransactionType()
TournamentName::class.java -> AnyTournamentName()
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>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance()
realm.refresh()
val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
@ -85,9 +114,12 @@ sealed class QueryCondition : FilterElementRow {
ANY,
ALL,
MORE,
MORE_OR_EQUAL,
LESS,
LESS_OR_EQUAL,
EQUALS,
TRUE,
NOTNULL
;
}
@ -95,16 +127,17 @@ sealed class QueryCondition : FilterElementRow {
val groupId: String
get() {
when (this.operator) {
Operator.MORE, Operator.LESS -> return "${this.operator.name.toLowerCase().capitalize()}$baseId"
return when (this.operator) {
Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return "${this.operator.name.toLowerCase().capitalize()}$baseId"
else -> this.baseId
}
return baseId
}
val id: List<String>
get() {
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 -> {}
}
return when (this) {
@ -121,24 +154,43 @@ sealed class QueryCondition : FilterElementRow {
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 var listOfValues: ArrayList<T>
abstract var listOfValues: MutableList<T>
abstract fun labelForValue(value: T, context: Context): String
open fun entityName(context: Context): String {
return getDisplayName(context)
}
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.listOfValues)
}
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 {
context.getString(it) + " "
} ?: ""
return when (listOfValues.size) {
return when (values.size) {
0 -> return NULL_TEXT
1, 2 -> prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ")
else -> "${listOfValues.size} $prefix ${entityName(context)}"
else -> prefix + values.joinToString(", ") { labelForValue(it, context) }
// else -> "${values.size} $prefix ${entityName(context)}"
}
}
@ -149,17 +201,41 @@ sealed class QueryCondition : FilterElementRow {
fun firstValue(context: Context): String? {
return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) }
}
}
abstract class SingleValue<T> : ListOfValues<T>() where T : Comparable<T> {
override var listOfValues = ArrayList<T>()
abstract var singleValue: T?
abstract class SingleValue<T>(value: T) : QueryCondition() where T : Comparable<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>() {
open var sign: Int = 1
override var operator: Operator = Operator.ANY
override var listOfValues: ArrayList<Double> = arrayListOf()
override var listOfValues = mutableListOf<Double>()
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues()
@ -172,7 +248,7 @@ sealed class QueryCondition : FilterElementRow {
abstract class ListOfInt : ListOfValues<Int>() {
override var operator: Operator = Operator.ANY
override var listOfValues: ArrayList<Int> = arrayListOf()
override var listOfValues = mutableListOf<Int>()
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues()
@ -185,7 +261,8 @@ sealed class QueryCondition : FilterElementRow {
abstract class ListOfString : ListOfValues<String>() {
override var operator: Operator = Operator.ANY
override var listOfValues = ArrayList<String>()
override var listOfValues = mutableListOf<String>()
override fun labelForValue(value: String, context: Context): String {
return value
}
@ -196,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 {
return value.shortDate()
}
override var listOfValues = ArrayList<Date>()
override var singleValue: Date?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
// override var listOfValues = mutableListOf<Date>()
override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue()
}
}
abstract class SingleInt : SingleValue<Int>() {
abstract class SingleInt(value: Int) : SingleValue<Int>(value) {
override fun labelForValue(value: Int, context: Context): String {
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) {
super.updateValueBy(filterCondition)
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() {
fun setObject(dataObject: T) {
@ -261,7 +316,7 @@ sealed class QueryCondition : FilterElementRow {
val completeLabel = when (listOfValues.size) {
0 -> NULL_TEXT
1, 2 -> {
listOfValues.map { labelForValue(realm, it) }.joinToString(", ")
listOfValues.joinToString(", ") { labelForValue(realm, it) }
}
else -> "${listOfValues.size} $entityName"
}
@ -269,6 +324,10 @@ sealed class QueryCondition : FilterElementRow {
return completeLabel
}
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context)
}
open fun entityName(realm: Realm, context: Context): String {
return entityName(context)
}
@ -277,34 +336,40 @@ sealed class QueryCondition : FilterElementRow {
val query = realm.where(entity)
return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
}
}
val objectId: String?
get() {
return this.listOfValues.firstOrNull()
}
}
interface DateTime {
val showTime: Boolean
}
abstract class DateQuery : SingleDate(), DateTime {
abstract class DateQuery(date: Date) : SingleDate(date), DateTime {
override val showTime: Boolean = false
override fun labelForValue(value: Date, context: Context): String {
return singleValue?.let {
if (showTime) {
it.shortTime()
} else {
it.shortDate()
}
} ?: NULL_TEXT
return if (showTime) {
singleValue.shortTime()
} else {
singleValue.shortDate()
}
}
}
abstract class TimeQuery : DateQuery() {
abstract class TimeQuery(date: Date) : DateQuery(date) {
override val showTime: Boolean = true
}
abstract class TrueQueryCondition : QueryCondition() {
override var operator: Operator = Operator.TRUE
}
abstract class NotNullQueryCondition : QueryCondition() {
override var operator: Operator = Operator.NOTNULL
}
object IsLive : TrueQueryCondition()
@ -338,50 +403,34 @@ sealed class QueryCondition : FilterElementRow {
}
}
class AnyTournamentName() : QueryDataCondition<TournamentName>() {
class AnyTournamentName : QueryDataCondition<TournamentName>() {
override val entity: Class<TournamentName> = TournamentName::class.java
constructor(tournamentName: TournamentName) : this() {
this.setObject(tournamentName)
}
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_names)
}
}
class AnyTournamentFeature() : QueryDataCondition<TournamentFeature>() {
class AnyTournamentFeature : QueryDataCondition<TournamentFeature>() {
override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature) : this() {
this.setObject(tournamentFeature)
}
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_features)
}
}
class AllTournamentFeature() : QueryDataCondition<TournamentFeature>() {
class AllTournamentFeature : QueryDataCondition<TournamentFeature>() {
override var operator = Operator.ALL
override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature) : this() {
this.setObject(tournamentFeature)
}
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_features)
}
}
class AnyLocation() : QueryDataCondition<Location>() {
class AnyLocation : QueryDataCondition<Location>() {
override val entity: Class<Location> = Location::class.java
constructor(location: Location) : this() {
this.setObject(location)
}
override fun entityName(context: Context): String {
return context.getString(R.string.locations)
}
@ -429,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 {
return context.getString(R.string.blinds)
return context.getString(R.string.stakes)
}
}
object Last : SingleInt() {
override var operator = Operator.EQUALS
override fun getDisplayName(context: Context): String {
//TODO update string "last %i"
return "${context.getString(R.string.last_i_records)} $singleValue"
override fun compareTo(other: ListOfValues<String>): Int {
return CodedStake(this.listOfValues.first()).compareTo(CodedStake(other.listOfValues.first()))
}
}
}
class NumberOfTable : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
@ -471,13 +522,13 @@ sealed class QueryCondition : FilterElementRow {
}
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)
2 -> context.getString(R.string.ordinal_suffix_second)
3 -> context.getString(R.string.ordinal_suffix_third)
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 {
@ -494,7 +545,7 @@ sealed class QueryCondition : FilterElementRow {
class TournamentNumberOfPlayer : ListOfInt() {
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 {
@ -502,20 +553,20 @@ sealed class QueryCondition : FilterElementRow {
}
}
class StartedFromDate : DateQuery() {
override var operator = Operator.MORE
class StartedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
}
class StartedToDate : DateQuery() {
override var operator = Operator.LESS
class StartedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
}
class EndedFromDate : DateQuery() {
override var operator = Operator.MORE
class EndedFromDate(date: Date) : DateQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
}
class EndedToDate : DateQuery() {
override var operator = Operator.LESS
class EndedToDate(date: Date) : DateQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
}
class AnyDayOfWeek : ListOfInt() {
@ -525,23 +576,23 @@ sealed class QueryCondition : FilterElementRow {
}
class AnyMonthOfYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize()
}
constructor(month: Int) : this() {
listOfValues = arrayListOf(month)
}
}
class AnyYear() : ListOfInt() {
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() {
listOfValues = arrayListOf(year)
}
override fun labelForValue(value: Int, context: Context): String {
return "$value"
}
}
object IsWeekDay : TrueQueryCondition()
@ -559,24 +610,19 @@ sealed class QueryCondition : FilterElementRow {
}
}
class PastDay : SingleInt() {
class PastDay(value: Int) : SingleInt(value) {
override var operator = Operator.EQUALS
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
override fun labelForValue(value: Int, context: Context): String {
return value.toString()
}
override fun entityName(context: Context): String {
return this.resId?.let {
" " + context.getString(it)
} ?: ""
override fun getDisplayNameWithValues(context: Context): String {
return context.getString(R.string.period_in_days_with_value, this.singleValue.toString())
}
}
class Duration : SingleInt() {
class Duration(value: Int) : SingleInt(value) {
override var operator = Operator.EQUALS
var minutes: Int?
var minutes: Int
get() {
return singleValue
}
@ -584,12 +630,9 @@ sealed class QueryCondition : FilterElementRow {
singleValue = value
}
val netDuration: Long?
val netDuration: Long
get() {
minutes?.let {
return (it * 60 * 1000).toLong()
}
return null
return (singleValue * 60 * 1000).toLong()
}
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
@ -600,23 +643,16 @@ sealed class QueryCondition : FilterElementRow {
}
}
class StartedFromTime() : TimeQuery() {
override var operator = Operator.MORE
constructor(date: Date) : this() {
singleValue = date
}
object DateNotNull : NotNullQueryCondition()
object EndDateNotNull : NotNullQueryCondition()
object BiggestBetNotNull : NotNullQueryCondition()
class StartedFromTime(date: Date) : TimeQuery(date) {
override var operator = Operator.MORE_OR_EQUAL
}
class EndedToTime() : TimeQuery() {
override var operator = Operator.LESS
constructor(date: Date) : this() {
singleValue = date
}
class EndedToTime(date: Date) : TimeQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
}
interface CustomFieldRelated {
@ -631,12 +667,8 @@ sealed class QueryCondition : FilterElementRow {
}
}
class CustomFieldQuery() : QueryDataCondition<CustomField>() {
class CustomFieldQuery : QueryDataCondition<CustomField>() {
override var entity: Class<CustomField> = CustomField::class.java
constructor(customField: CustomField) : this() {
this.setObject(customField)
}
}
open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated {
@ -658,7 +690,9 @@ sealed class QueryCondition : FilterElementRow {
val completeLabel = when (listOfValues.size) {
0 -> return NULL_TEXT
1, 2 -> {
return name + prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ")
return name + prefix + listOfValues.joinToString(", ") {
labelForValue(it, context)
}
}
else -> "${listOfValues.size} $prefix $name"
}
@ -714,9 +748,16 @@ sealed class QueryCondition : FilterElementRow {
): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
if (BuildConfig.DEBUG) {
fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
// if (BuildConfig.DEBUG) {
// 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
when (this) {
@ -754,15 +795,12 @@ sealed class QueryCondition : FilterElementRow {
.lessThanOrEqualTo(fieldName, calendar.time.endOfDay())
}
is PastDay -> {
singleValue?.let {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.DAY_OF_YEAR, -it)
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
.lessThanOrEqualTo(fieldName, startDate.endOfDay())
}
return realmQuery
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.DAY_OF_YEAR, -singleValue)
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
.lessThanOrEqualTo(fieldName, startDate.endOfDay())
}
is DuringThisWeek -> {
val calendar = Calendar.getInstance()
@ -792,32 +830,25 @@ sealed class QueryCondition : FilterElementRow {
}
is StartedFromTime -> {
val calendar = Calendar.getInstance()
singleValue?.let {
calendar.time = it
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is EndedToTime) {
otherQueryCondition.singleValue?.let { endTime ->
calendar.time = endTime
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
}
}
calendar.time = singleValue
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is EndedToTime) {
calendar.time = otherQueryCondition.singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
}
return realmQuery
}
is EndedToTime -> {
val calendar = Calendar.getInstance()
singleValue?.let {
calendar.time = singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is StartedFromTime) {
otherQueryCondition.singleValue?.let { startTime ->
calendar.time = startTime
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
}
}
calendar.time = singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is StartedFromTime) {
calendar.time = otherQueryCondition.singleValue
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
}
return realmQuery
}
else -> {}
}
if (this is CustomFieldRelated) {
@ -838,58 +869,62 @@ sealed class QueryCondition : FilterElementRow {
return when (operator) {
Operator.EQUALS -> {
when (this) {
is SingleDate -> realmQuery.equalTo(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleInt -> realmQuery.equalTo(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleDate -> realmQuery.equalTo(fieldName, singleValue)
is SingleInt -> realmQuery.equalTo(fieldName, singleValue)
is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign)
is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first())
else -> realmQuery
}
}
Operator.MORE -> {
Operator.MORE_OR_EQUAL -> {
when (this) {
is SingleDate -> realmQuery.greaterThanOrEqualTo(
fieldName,
singleValue?.startOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.greaterThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue.startOfDay())
is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is SingleInt -> realmQuery.greaterThan(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.greaterThanOrEqualTo(fieldName, netDuration)
is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
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 NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1)
is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.LESS -> {
Operator.LESS_OR_EQUAL -> {
when (this) {
is SingleDate -> realmQuery.lessThanOrEqualTo(
fieldName,
singleValue?.endOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.lessThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue.endOfDay())
is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is SingleInt -> realmQuery.lessThan(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.lessThanOrEqualTo(fieldName, netDuration)
is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
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 NetAmountLost -> {
realmQuery.greaterThan(fieldName, listOfValues.first() * -1)
@ -928,6 +963,7 @@ sealed class QueryCondition : FilterElementRow {
else -> realmQuery
}
}
Operator.NOTNULL -> realmQuery.isNotNull(fieldName)
else -> realmQuery
}
}
@ -944,8 +980,8 @@ sealed class QueryCondition : FilterElementRow {
is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> {
when (this.operator) {
Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.MORE, Operator.MORE_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS, Operator.LESS_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> RowViewType.TITLE_CHECK.ordinal
}
}
@ -958,8 +994,8 @@ sealed class QueryCondition : FilterElementRow {
is PastDay -> BottomSheetType.EDIT_TEXT
else -> {
when (this.operator) {
Operator.MORE -> BottomSheetType.EDIT_TEXT
Operator.LESS -> BottomSheetType.EDIT_TEXT
Operator.MORE, Operator.MORE_OR_EQUAL -> BottomSheetType.EDIT_TEXT
Operator.LESS, Operator.LESS_OR_EQUAL -> BottomSheetType.EDIT_TEXT
else -> BottomSheetType.NONE
}
}
@ -986,27 +1022,27 @@ sealed class QueryCondition : FilterElementRow {
is IsWeekDay -> R.string.week_days
is IsWeekEnd -> R.string.weekend
is PastDay -> R.string.period_in_days
is TournamentNumberOfPlayer -> {
when (this.operator) {
Operator.MORE -> R.string.minimum
Operator.LESS -> R.string.maximum
else -> null
}
}
is NetAmountWon -> {
when (this.operator) {
Operator.MORE -> R.string.won_amount_more_than
Operator.LESS -> R.string.won_amount_less_than
else -> null
}
}
is NetAmountLost -> {
when (this.operator) {
Operator.MORE -> R.string.lost_amount_more_than
Operator.LESS -> R.string.lost_amount_less_than
else -> null
}
}
// is TournamentNumberOfPlayer -> {
// when (this.operator) {
// Operator.MORE -> R.string.minimum
// Operator.LESS -> R.string.maximum
// else -> null
// }
// }
// is NetAmountWon -> {
// when (this.operator) {
// Operator.MORE -> R.string.won_amount_more_than
// Operator.LESS -> R.string.won_amount_less_than
// else -> null
// }
// }
// is NetAmountLost -> {
// when (this.operator) {
// Operator.MORE -> R.string.lost_amount_more_than
// Operator.LESS -> R.string.lost_amount_less_than
// else -> null
// }
// }
is TournamentFinalPosition -> {
when (this.operator) {
Operator.MORE -> R.string.minimum
@ -1016,8 +1052,10 @@ sealed class QueryCondition : FilterElementRow {
}
else -> {
when (this.operator) {
Operator.MORE -> R.string.more_than
Operator.LESS -> R.string.less_than
Operator.MORE_OR_EQUAL -> R.string.more_or_equal_sign
Operator.MORE -> R.string.more_sign
Operator.LESS_OR_EQUAL -> R.string.less_or_equal_sign
Operator.LESS -> R.string.less_sign
else -> null
}
}

@ -0,0 +1,108 @@
package net.pokeranalytics.android.model.handhistory
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Game
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
class HandSetup {
companion object {
/***
* Returns a HandSetup instance using a [configurationId], the id of a Realm object,
* Session or HandHistory, used to later configure the HandHistory
*/
fun from(configurationId: String?, attached: Boolean, realm: Realm): HandSetup {
return if (configurationId != null) {
val handSetup = HandSetup()
val hh = realm.findById(HandHistory::class.java, configurationId)
if (hh != null) {
handSetup.configure(hh)
}
val session = realm.findById(Session::class.java, configurationId)
if (session != null) {
handSetup.configure(session, attached)
}
handSetup
} else {
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) {
this.blinds = handHistory.blinds
this.bigBlindAnte = handHistory.bigBlindAnte
this.ante = handHistory.ante
this.tableSize = handHistory.numberOfPlayers
}
/***
* Configures the Hand Setup with a [session]
* [attached] denotes if the HandHistory must be directly linked to the session
*/
private fun configure(session: Session, attached: Boolean) {
if (attached) {
this.session = session
}
if (session.endDate == null) {
this.game = session.game // we don't want to force the max number of cards if unsure
}
this.type = session.sessionType
this.blinds = session.cgBlinds
this.ante = session.cgAnte
this.tableSize = session.tableSize
val blindValues = session.blindValues
if (blindValues.size > 2) {
this.straddlePositions = Position.positionsPerPlayers(10).drop(2).take(blindValues.size - 2).toMutableList()
}
}
/***
* This method sorts the straddle positions in their natural order
* If the straddle position contains the button, we're usually in a Mississipi straddle,
* meaning the BUT straddles, then CO, then HJ...
* Except if it goes to UTG, in which case we don't know if we're in standard straddle, or Mississipi
* We use the first straddled position to sort out this case
*/
fun setStraddlePositions(firstStraddlePosition: Position, positions: LinkedHashSet<Position>) {
var sortedPosition = positions.sortedBy { it.ordinal }
if (positions.contains(Position.BUT) && firstStraddlePosition != Position.UTG) {
sortedPosition = sortedPosition.reversed()
}
Timber.d("sortedPosition = $sortedPosition")
this.straddlePositions = sortedPosition.toMutableList()
Timber.d("this.straddlePositions = ${this.straddlePositions}")
}
}

@ -0,0 +1,53 @@
package net.pokeranalytics.android.model.handhistory
import android.content.Context
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.view.RowRepresentable
import java.util.*
enum class Position(var value: String) : RowRepresentable {
SB("SB"),
BB("BB"),
UTG("UTG"),
UTG1("UTG+1"),
UTG2("UTG+2"),
UTG3("UTG+3"),
MP("MP"),
HJ("HJ"),
CO("CO"),
BUT("BUT");
companion object {
fun positionsPerPlayers(playerCount: Int) : LinkedHashSet<Position> {
return when(playerCount) {
2 -> linkedSetOf(SB, BB)
3 -> linkedSetOf(SB, BB, BUT)
4 -> linkedSetOf(SB, BB, UTG, BUT)
5 -> linkedSetOf(SB, BB, UTG, CO, BUT)
6 -> linkedSetOf(SB, BB, UTG, HJ, CO, BUT)
7 -> linkedSetOf(SB, BB, UTG, MP, HJ, CO, BUT)
8 -> linkedSetOf(SB, BB, UTG, UTG1, MP, HJ, CO, BUT)
9 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, MP, HJ, CO, BUT)
10 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, UTG3, MP, HJ, CO, BUT)
else -> throw PAIllegalStateException("Unmanaged number of players")
}
}
}
val shortValue: String
get() {
return when (this) {
UTG1 -> "+1"
UTG2 -> "+2"
UTG3 -> "+3"
else -> this.value
}
}
override fun getDisplayName(context: Context): String {
return this.value
}
}

@ -0,0 +1,50 @@
package net.pokeranalytics.android.model.handhistory
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.modules.handhistory.replayer.HandStep
enum class Street : HandStep {
PREFLOP,
FLOP,
TURN,
RIVER,
SUMMARY;
override val street: Street = this
val totalBoardCards: Int
get() {
return when (this) {
PREFLOP -> 0
FLOP -> 3
TURN -> 4
RIVER, SUMMARY -> 5
}
}
val resId: Int
get() {
return when (this) {
PREFLOP -> R.string.street_preflop
FLOP -> R.string.street_flop
TURN -> R.string.street_turn
RIVER -> R.string.street_river
SUMMARY -> R.string.summary
}
}
val next: Street
get() {
return values()[this.ordinal + 1]
}
val previous: Street?
get() {
return when (this) {
PREFLOP -> null
else -> values()[this.ordinal - 1]
}
}
}

@ -5,7 +5,6 @@ import io.realm.Realm
import io.realm.RealmModel
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.ui.view.RowRepresentable
enum class SaveValidityStatus {
VALID,
@ -23,7 +22,7 @@ enum class DeleteValidityStatus {
/**
* An interface to group object which are managed by the database
*/
interface Manageable : Savable, Deletable, Editable
interface Manageable : Savable, Deletable
interface NameManageable : Manageable {
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
*/

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

@ -19,10 +19,10 @@ interface TimeFilterable {
startDate?.let {
val cal = Calendar.getInstance()
cal.time = it
dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
month = cal.get(Calendar.MONTH)
year = cal.get(Calendar.YEAR)
this.dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
this.dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
this.month = cal.get(Calendar.MONTH)
this.year = cal.get(Calendar.YEAR)
}
}
}

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

@ -2,38 +2,76 @@ package net.pokeranalytics.android.model.migrations
import android.content.Context
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.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.Preferences
import java.text.NumberFormat
class Patcher {
companion object {
fun patchAll(context: Context) {
fun patchAll(application: PokerAnalyticsApplication) {
val context = application.applicationContext
// 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) {
patchSessionSet()
}
Preferences.executeOnce(Preferences.Keys.PATCH_BREAK, context) {
patchBreaks()
}
Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) {
patchDefaultTransactionTypes(context)
}
Preferences.executeOnce(Preferences.Keys.PATCH_BLINDS_FORMAT, context) {
patchBlindFormat()
}
Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) {
patchStakes()
}
Preferences.executeOnce(Preferences.Keys.PATCH_NEGATIVE_LIMITS, context) {
patchNegativeLimits()
}
Preferences.executeOnce(Preferences.Keys.CLEAN_BLINDS_FILTERS, context) {
cleanBlindsFilters()
}
Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
patchMissingTransactionTypes(context)
}
Preferences.executeOnce(Preferences.Keys.PATCH_ZERO_TABLE, context) {
patchZeroTable()
}
Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) {
patchRatedAmounts()
}
patchPerformances(application)
}
private fun patchMissingTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance()
val lockedTypes = realm.where(TransactionType::class.java).equalTo("lock", true).findAll()
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)
}
}
val transactionTypes = TransactionType.Value.values()
realm.executeTransaction {
Seed.createDefaultTransactionTypes(transactionTypes, context, realm)
}
realm.close()
@ -44,18 +82,19 @@ class Patcher {
val realm = Realm.getDefaultInstance()
val sets = realm.where(SessionSet::class.java).findAll()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsCash))
val results = realm.where(Result::class.java).findAll()
val results = realm.where(Result::class.java).findAll()
realm.executeTransaction {
sets.forEach {
it.computeStats()
}
sessions.forEach {
it.formatBlinds()
}
results.forEach {
it.computeNumberOfRebuy()
}
sessions.forEach {
it.generateStakes()
it.defineHighestBet()
}
results.forEach {
it.computeNumberOfRebuy()
}
}
realm.close()
@ -76,17 +115,120 @@ class Patcher {
realm.close()
}
private fun patchBlindFormat() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session ->
session.formatBlinds()
}
}
realm.close()
}
private fun patchStakes() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session ->
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()
}
/*
02/09/19: A bug with the session set management made them kept instead of deleted,
thus making duration calculation wrong
*/
private fun patchSessionSet() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(SessionSet::class.java).findAll().deleteAllFromRealm()
val sessions = realm.where(Session::class.java).isNotNull("startDate").isNotNull("endDate").findAll()
sessions.forEach { session ->
SessionSetManager.updateTimeline(session)
}
}
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()
}
}
}

@ -2,12 +2,14 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber
import java.util.*
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
val schema = realm.schema
@ -19,7 +21,8 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 0) {
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 {
it.setNullable("filterName", true)
it.setNullable("sectionName", true)
@ -81,7 +84,8 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 3) {
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++
}
@ -115,7 +119,8 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("CustomField")?.let {
it.addField("type", Integer::class.java).setNullable("type", false)
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)
}
@ -132,7 +137,8 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("ReportSetup")?.let {
it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true)
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")
schema.get("Filter")?.let { filterSchema ->
it.addRealmObjectField("filter", filterSchema)
@ -151,13 +157,192 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("TransactionType")?.addField("useCount", Int::class.java)
currentVersion++
}
// Migrate to version 8
if (currentVersion == 7) {
schema.create("Comment")?.let { commentSchema ->
commentSchema.addField("id", String::class.java).setRequired("id", true)
commentSchema.addPrimaryKey("id")
commentSchema.addField("content", String::class.java).setRequired("content", true)
commentSchema.addField("date", Date::class.java).setRequired("date", true)
schema.get("Player")?.let {
it.addField("summary", String::class.java).setRequired("summary", true)
it.addField("color", Int::class.java).setNullable("color", true)
it.addField("picture", String::class.java)
it.addRealmListField("comments", commentSchema)
}
}
currentVersion++
}
// Migrate to version 9
if (currentVersion == 8) {
schema.get("HandHistory")?.let { hhSchema ->
schema.get("Session")?.let { sessionSchema ->
sessionSchema.removeField("hands")
hhSchema.addRealmObjectField("session", sessionSchema)
} ?: throw PAIllegalStateException("Session schema not found")
hhSchema.addField("smallBlind", Double::class.java).setRequired("smallBlind", false)
hhSchema.addField("bigBlind", Double::class.java).setRequired("bigBlind", false)
hhSchema.addField("ante", Double::class.java)
hhSchema.addField("bigBlindAnte", Boolean::class.java)
hhSchema.addField("numberOfPlayers", Int::class.java)
hhSchema.addField("comment", String::class.java)
hhSchema.addField("heroIndex", Int::class.java).setRequired("heroIndex", false)
hhSchema.addField("dayOfWeek", Integer::class.java)
hhSchema.addField("month", Integer::class.java)
hhSchema.addField("year", Integer::class.java)
hhSchema.addField("dayOfMonth", Integer::class.java)
val cardSchema = schema.create("Card")
cardSchema.addField("value", Int::class.java).setRequired("value", false)
cardSchema.addField("suitIdentifier", Int::class.java)
.setRequired("suitIdentifier", false)
cardSchema.addField("index", Int::class.java)
hhSchema.addRealmListField("board", cardSchema)
val actionSchema = schema.create("Action")
actionSchema.addField("streetIdentifier", Int::class.java)
actionSchema.addField("index", Int::class.java)
actionSchema.addField("position", Int::class.java)
actionSchema.addField("typeIdentifier", Int::class.java)
.setRequired("typeIdentifier", false)
actionSchema.addField("amount", Double::class.java).setRequired("amount", false)
actionSchema.addField("effectiveAmount", Double::class.java)
actionSchema.addField("positionRemainingStack", Double::class.java)
.setRequired("positionRemainingStack", false)
hhSchema.addRealmListField("actions", actionSchema)
val playerSetupSchema = schema.create("PlayerSetup")
schema.get("Player")?.let {
playerSetupSchema.addRealmObjectField("player", it)
} ?: throw PAIllegalStateException("Session schema not found")
playerSetupSchema.addField("position", Int::class.java)
playerSetupSchema.addField("stack", Double::class.java).setRequired("stack", false)
playerSetupSchema.addRealmListField("cards", cardSchema)
hhSchema.addRealmListField("playerSetups", playerSetupSchema)
val wonPotSchema = schema.create("WonPot")
wonPotSchema.addField("position", Int::class.java)
wonPotSchema.addField("amount", Double::class.java)
hhSchema.addRealmListField("winnerPots", wonPotSchema)
}
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 {
return other is RealmMigration
}
override fun hashCode(): Int {
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.SaveValidityStatus
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.BankrollRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.RowUpdatable
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 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
override var id = UUID.randomUUID().toString()
@ -35,6 +60,12 @@ open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
@LinkingObjects("bankroll")
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
var currency: Currency? = null
@ -50,25 +81,6 @@ open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
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 {
return realm.where<Session>().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 {
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
get() {
this.currency?.code?.let {

@ -0,0 +1,79 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.Manageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
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.util.NULL_TEXT
import java.util.*
open class Comment : RealmObject(), Manageable, RowRepresentable, RowUpdatable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
var content: String = ""
var date: Date = Date()
@Ignore
override val realmObjectClass: Class<out Identifiable> = Comment::class.java
@Ignore
override val viewType: Int = RowViewType.CONTENT.ordinal
@Ignore
override val bottomSheetType: BottomSheetType = BottomSheetType.EDIT_TEXT_MULTI_LINES
// @Ignore
// override val valueCanBeClearedWhenEditing: Boolean = false
override fun localizedTitle(context: Context): String {
return context.getString(R.string.comment)
}
override fun getDisplayName(context: Context): String {
return if (content.isNotEmpty()) content else NULL_TEXT
}
// override fun startEditing(dataSource: Any?, parent: Fragment?) {
// if (parent == null) return
// if (parent !is RowRepresentableDelegate) return
// val data = RowEditableDataSource()
// data.append(this.content, R.string.value)
// InputFragment.buildAndShow(this, parent, data, isDeletable = true)
// }
override fun updateValue(value: Any?, row: RowRepresentable) {
this.content = value as String? ?: ""
}
override fun isValidForSave(): Boolean {
return true
}
override fun alreadyExists(realm: Realm): Boolean {
return realm.where(this::class.java).notEqualTo("id", this.id).findAll().isNotEmpty()
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled")
}
override fun isValidForDelete(realm: Realm): Boolean {
return true
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
return R.string.cf_entry_delete_popup_message
}
}

@ -4,29 +4,27 @@ import io.realm.RealmObject
import net.pokeranalytics.android.model.filter.Filterable
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
var bbNet: BB = 0.0
var bbNet: BB = 0.0
var hasBigBlind: Int = 0
var hasBigBlind: Int = 0
var isPositive: Int = 0
var isPositive: Int = 0
var ratedBuyin: Double = 0.0
var ratedBuyin: Double = 0.0
var estimatedHands: Double = 0.0
var estimatedHands: Double = 0.0
var bbPer100Hands: BB = 0.0
// var sessionSet: SessionSet? = null
var bbPer100Hands: BB = 0.0
var session: Session? = null
fun updateWith(session: Session) {
var ratedTips: Double = 0.0
// this.sessionSet = session.sessionSet
fun updateWith(session: Session) {
val rate = session.bankroll?.currency?.rate ?: 1.0
@ -34,12 +32,14 @@ open class ComputableResult() : RealmObject(), Filterable {
this.ratedNet = result.net * rate
this.isPositive = result.isPositive
this.ratedBuyin = (result.buyin ?: 0.0) * rate
this.ratedTips = (result.tips ?: 0.0) * rate
}
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.bbPer100Hands = session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
this.bbPer100Hands =
session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
}
@ -50,16 +50,17 @@ open class ComputableResult() : RealmObject(), Filterable {
IS_POSITIVE("isPositive"),
RATED_BUYIN("ratedBuyin"),
ESTIMATED_HANDS("estimatedHands"),
BB_PER100HANDS("bbPer100Hands")
RATED_TIPS("ratedTips"),
// BB_PER100HANDS("bbPer100Hands")
}
companion object {
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "session.$it"
}
return null
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "session.$it"
}
return null
}
}

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
@ -22,6 +23,17 @@ open class Currency : RealmObject() {
* The currency code of the currency, i.e. USD, EUR...
*/
var code: String? = null
set(value) {
try {
if (value != null) {
java.util.Currency.getInstance(value) // test validity of code
}
field = value
} catch (e: Exception) {
// make app crash earlier than later, possibly to show an error message to the user in the future
throw PAIllegalStateException(e.localizedMessage ?: e.toString())
}
}
/**
* The rate of the currency with the main currency

@ -18,16 +18,34 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
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.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomFieldRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rows.CustomFieldPropertiesRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource {
companion object {
fun getOrCreate(realm: Realm, name: String, type: Int): CustomField {
val cf = realm.where(CustomField::class.java).equalTo("name", name).findFirst()
return if (cf != null) {
cf
} else {
val customField = CustomField()
customField.name = name
customField.type = type
realm.copyToRealm(customField)
}
}
}
@Ignore
override val realmObjectClass: Class<out Identifiable> = CustomField::class.java
@ -94,13 +112,9 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
@Ignore
private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList()
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
@Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf()
//helper
val isListType: Boolean
@ -113,14 +127,6 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
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>? {
return rowRepresentation
}
@ -128,8 +134,8 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
CustomFieldRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
CustomFieldPropertiesRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldPropertiesRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
}
}
@ -161,13 +167,6 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
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) {
if (isValid) {
@ -176,7 +175,7 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) {
is CustomFieldEntry -> row.editingDescriptors(
mapOf(
@ -187,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
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.add(CustomFieldRow.TYPE)
rows.add(CustomFieldPropertiesRow.TYPE)
if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) {
if (entries.isNotEmpty()) {
@ -273,7 +250,6 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
fun cleanupEntries() { // called when saving the custom field
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
this.entriesToDelete.forEach { // entries are out of realm
realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
@ -283,6 +259,19 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
this.entriesToDelete.clear()
}
fun getOrCreateEntry(realm: Realm, value: String): CustomFieldEntry {
this.entries.find { it.value == value }?.let {
Timber.d("L>> get")
return it
} ?: run {
Timber.d("L>> create")
val entry = realm.copyToRealm(CustomFieldEntry())
entry.value = value
this.entries.add(entry)
return entry
}
}
/**
* Clean the entries if the type is not a list & remove the deleted entries from realm
*/
@ -309,9 +298,50 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
val criteria: Criteria
get() {
return when (this.type) {
CustomField.Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id)
Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id)
else -> Criteria.ValueCustomFields(this.id)
}
}
@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.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.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.toCurrency
@ -26,7 +27,7 @@ import java.util.*
import java.util.Currency
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable {
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = CustomFieldEntry::class.java

@ -11,12 +11,10 @@ import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.interfaces.FilterableType
import net.pokeranalytics.android.ui.view.ImageDecorator
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterItemRow
import timber.log.Timber
import java.util.*
@ -25,7 +23,7 @@ import java.util.*
* 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
*/
open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, CountableUsage, ImageDecorator {
open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, UsageCountable, ImageDecorator {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Filter::class.java
@ -37,7 +35,6 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
val filter = Filter()
filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier
return filter
//return realm.copyToRealm(filter)
}
inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> {
@ -46,7 +43,6 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
sortField?.let {
realmQuery = realmQuery.sort(it)
}
// val desc = realmQuery.description
return realmQuery.findAll()
}
}
@ -91,25 +87,25 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
return FilterableType.ALL
}
fun createOrUpdateFilterConditions(filterConditionRows: ArrayList<QueryCondition>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.id }}")
fun createOrUpdateFilterConditions(filterConditionRows: List<FilterItemRow>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.queryCondition?.id }}")
Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}")
filterConditionRows
.map {
it.groupId
.mapNotNull {
it.queryCondition?.groupId
}
.distinct()
.forEach { groupId ->
filterConditionRows
.filter {
it.groupId == groupId
it.queryCondition?.groupId == groupId
}
.apply {
Timber.d("list of querys: ${this.map { it.id }}")
val casted = arrayListOf<QueryCondition>()
casted.addAll(this)
val newFilterCondition = FilterCondition(casted)
val conditions = this.mapNotNull { it.queryCondition }
Timber.d("list of querys: ${conditions.map { it.id }}")
val newFilterCondition = FilterCondition(conditions, this.first().filterSectionRow)
val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
}
@ -137,38 +133,28 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}")
Timber.d("list of contains ${filterElementRow.id}")
val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first())
Timber.d("list of : $contained")
Timber.d("is contained: $contained")
return contained
}
/**
* Get the saved value for the given [filterElementRow]
*/
fun loadValueForElement(filterElementRow: QueryCondition) {
val filtered = filterConditions.filter {
it.queryCondition.id == filterElementRow.id
}
if (filtered.isNotEmpty()) {
return filterElementRow.updateValueBy(filtered.first())
fun filterCondition(filterElementRow: QueryCondition): FilterCondition? {
return filterConditions.firstOrNull {
it.queryCondition.id.contains(filterElementRow.id.first())
}
}
inline fun <reified T : Filterable> query(firstField: String? = null, secondField: String? = null): RealmQuery<T> {
inline fun <reified T : Filterable> query(firstField: String? = null, vararg remainingFields: String): RealmQuery<T> {
val realmQuery = realm.where<T>()
if (firstField != null && secondField != null) {
return this.query.queryWith(realmQuery).distinct(firstField, secondField)
}
if (firstField != null) {
return this.query.queryWith(realmQuery).distinct(firstField)
return this.query.queryWith(realmQuery).distinct(firstField, *remainingFields)
}
return this.query.queryWith(realmQuery)
}
inline fun <reified T : Filterable> results(firstField: String? = null, secondField: String? = null): RealmResults<T> {
return this.query<T>(firstField, secondField).findAll()
inline fun <reified T : Filterable> results(firstField: String? = null, vararg remainingFields: String): RealmResults<T> {
return this.query<T>(firstField, *remainingFields).findAll()
}
val query: Query
@ -208,11 +194,9 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
}
override fun updateValue(value: Any?, row: RowRepresentable) {
realm.executeTransaction {
val newName = value as String? ?: ""
if (newName.isNotEmpty()) {
name = newName
}
val newName = value as String? ?: ""
if (newName.isNotEmpty()) {
name = newName
}
}
}

@ -2,19 +2,21 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import java.util.*
import kotlin.collections.ArrayList
open class FilterCondition() : RealmObject() {
private constructor(filterName:String, sectionName:String) : this() {
private constructor(filterName: String, sectionName: String) : this() {
this.filterName = filterName
this.sectionName = sectionName
}
constructor(filterElementRows: ArrayList<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()
this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName
this.operator = row.operator.ordinal
@ -22,11 +24,12 @@ open class FilterCondition() : RealmObject() {
this.stringValue = row.customFieldId
}
when (row) {
is QueryCondition.SingleInt -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing)
is QueryCondition.SingleDate -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing)
is QueryCondition.SingleInt -> this.setValue(row.singleValue)
is QueryCondition.SingleDate -> this.setValue(row.singleValue)
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.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues })
else -> {}
}
}
@ -48,7 +51,10 @@ open class FilterCondition() : RealmObject() {
var stringValue: String? = 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) {
Int::class -> ArrayList<T>().apply { intValues?.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) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
@ -91,4 +107,5 @@ open class FilterCondition() : RealmObject() {
fun setValue(value:String) {
stringValue = value
}
}

@ -8,20 +8,21 @@ import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rowrepresentable.GameRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.GamePropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, CountableUsage {
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable, UsageCountable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Game::class.java
@ -30,7 +31,6 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
// rows.addAll(GameRow.values())
rows
}
}
@ -62,21 +62,25 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
}
override fun adapterRows(): List<RowRepresentable>? {
return Game.rowRepresentation
return rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
GameRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.stringForRow(row)
GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) {
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
}
}
@ -84,7 +88,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
GameRow.SHORT_NAME -> this.shortName = value as String? ?: ""
GamePropertiesRow.SHORT_NAME -> this.shortName = value as String? ?: ""
}
}
@ -103,4 +107,30 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("game.id", id).findAll().isEmpty()
}
val playerHandMaxCards: Int?
get() {
return when {
isHoldem -> 2
isOmaha5 -> 5
isOmaha4 -> 4
else -> null
}
}
private val isHoldem: Boolean
get() {
return name.contains("texas", ignoreCase = true) || name.contains("holdem", ignoreCase = true) || name.contains("hold'em", ignoreCase = true) || name.contains("HE")
}
private val isOmaha4: Boolean
get() {
return name.contains("omaha", ignoreCase = true) || name.contains("PLO", ignoreCase = true)
}
private val isOmaha5: Boolean
get() {
return name.contains("5")
}
}

@ -1,16 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import java.util.*
open class HandHistory : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
// the date of the hand history
var date: Date = Date()
}

@ -12,11 +12,12 @@ import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
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.*
open class Location : RealmObject(), NameManageable, RowRepresentable {
open class Location : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
@Ignore
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
}
}
}

@ -1,15 +1,101 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
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.rows.PlayerPropertiesRow
import net.pokeranalytics.android.util.RANDOM_PLAYER
import java.util.*
open class Player : RealmObject() {
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable {
@PrimaryKey
var id = UUID.randomUUID().toString()
override var id = UUID.randomUUID().toString()
// The name of the player
var name: String = ""
override var name: String = ""
}
// New fields
var summary: String = ""
var color: Int? = null
var picture: String? = null
var comments: RealmList<Comment> = RealmList()
@Ignore
override val realmObjectClass: Class<out Identifiable> = Player::class.java
@Ignore
override val viewType: Int = RowViewType.ROW_PLAYER.ordinal
override fun isValidForDelete(realm: Realm): Boolean {
//TODO
return true
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when(status) {
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_user_error
SaveValidityStatus.DATA_INVALID -> R.string.user_empty_field_error
else -> super.getFailedSaveMessage(status)
}
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
//TODO
return R.string.relationship_error
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerPropertiesRow.IMAGE -> this.picture = value as? String
}
}
/**
* Return if the player has a picture
*/
fun hasPicture(): Boolean {
return picture != null && picture?.isNotEmpty() == true
}
val initials: String
get() {
return if (this.name.isNotEmpty()) {
val playerData = this.name.split(" ")
when {
playerData.size > 1 -> {
playerData[0].first().toString() + playerData[1].first().toString()
}
this.name.length > 1 -> {
this.name.substring(0, 2)
}
else -> {
this.name.substring(0, this.name.length)
}
}
} else {
RANDOM_PLAYER
}
}
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.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria
@ -30,7 +31,7 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
var name: String = ""
// 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
@ -64,30 +65,30 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
/**
* Returns the Options based on the ReportSetup parameters
*/
val options: Calculator.Options
get() {
fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options {
val realm = Realm.getDefaultInstance()
val stats = this.statIds.map { Stat.valueByIdentifier(it) }
val stats = this.statIds.map { Stat.valueByIdentifier(it) }
// Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
val cfCriteria = customFields.map { it.criteria }
// Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
val allCriteria = mutableListOf<Criteria>()
allCriteria.addAll(criteria)
allCriteria.addAll(cfCriteria)
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
return Calculator.Options(
display = Calculator.Options.Display.values()[this.display],
val cfCriteria = customFields.map { it.criteria }
val allCriteria = mutableListOf<Criteria>()
allCriteria.addAll(criteria)
allCriteria.addAll(cfCriteria)
return Calculator.Options(
stats = stats,
progressValues = reportDisplay.progressValues,
criterias = allCriteria,
filter = this.filter,
userGenerated = true,
reportSetupId = this.id
)
}
)
}
// Deletable

@ -6,7 +6,6 @@ import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import io.realm.annotations.RealmClass
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
@ -30,7 +29,7 @@ open class Result : RealmObject(), Filterable {
set(value) {
field = value
this.computeNumberOfRebuy()
this.computeNet()
this.computeNet(true)
}
/**
@ -39,7 +38,7 @@ open class Result : RealmObject(), Filterable {
var cashout: Double? = null
set(value) {
field = value
this.computeNet()
this.computeNet(true)
if (value != null) {
this.session?.end()
}
@ -51,18 +50,18 @@ open class Result : RealmObject(), Filterable {
var netResult: Double? = null
set(value) {
this.session?.bankroll?.let { bankroll ->
if (bankroll.live) {
throw PAIllegalStateException("Can't set net result on a live bankroll")
}
} ?: run {
throw PAIllegalStateException("Session doesn't have any bankroll")
}
// this.session?.bankroll?.let { bankroll ->
// if (bankroll.live) {
// throw PAIllegalStateException("Can't set net result on a live bankroll")
// }
// } ?: run {
// throw PAIllegalStateException("Session doesn't have any bankroll")
// }
field = value
this.computeNet()
this.computeNet(false)
if (value != null) {
this.session.end()
this.session?.end()
}
}
@ -76,6 +75,10 @@ open class Result : RealmObject(), Filterable {
* Tips
*/
var tips: Double? = null
set(value) {
field = value
this.session?.computeStats()
}
// The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList()
@ -99,16 +102,37 @@ open class Result : RealmObject(), Filterable {
/**
* Returns 1 if the session is positive
*/
@Ignore
val isPositive: Int = if (this.net >= 0.0) 1 else 0
val isPositive: Int
get() {
return if (session?.isTournament() == true) {
if ((this.cashout ?: -1.0) > 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else {
if (this.net >= 0.0) 1 else 0
}
}
// Computes the Net
private fun computeNet() {
val transactionsSum = transactions.sumByDouble { it.amount }
private fun computeNet(withBuyin: Boolean? = null) {
val transactionsSum = transactions.sumOf { it.amount }
// choose the method to compute the net
var useBuyin = withBuyin ?: true
if (withBuyin == null) {
if (netResult != null) {
useBuyin = false
} else if (buyin != null || cashout != null) {
useBuyin = true
} else {
this.session?.let { session ->
if (session.isCashGame() && !session.isLive) {
useBuyin = false
}
}
}
}
val isLive = this.session?.isLive ?: true
if (isLive) {
if (useBuyin) {
val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum
@ -126,7 +150,7 @@ open class Result : RealmObject(), Filterable {
fun computeNumberOfRebuy() {
this.session?.let {
if (it.isCashGame()) {
it.cgBigBlind?.let { bb ->
it.cgBiggestBet?.let { bb ->
if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else {

@ -61,9 +61,9 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override var netDuration: Long = 0L
fun computeStats() {
this.ratedNet = this.sessions?.sumByDouble { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumByDouble { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumByDouble { it.bbNet } ?: 0.0
this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L
}
@ -122,15 +122,15 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override fun formattedValue(stat: Stat) : TextFormat {
return when (stat) {
Stat.NET_RESULT, Stat.AVERAGE -> stat.format(this.ratedNet, currency = null)
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.format(this.hourlyDuration, currency = null)
Stat.HOURLY_RATE -> stat.format(this.hourlyRate, currency = null)
Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, currency = null)
Stat.HOURLY_RATE_BB -> stat.format(this.bbHourlyRate, currency = null)
Stat.NET_RESULT, Stat.AVERAGE -> stat.textFormat(this.ratedNet, currency = null)
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.textFormat(this.hourlyDuration, currency = null)
Stat.HOURLY_RATE -> stat.textFormat(this.hourlyRate, currency = null)
Stat.HANDS_PLAYED -> stat.textFormat(this.estimatedHands, currency = null)
Stat.HOURLY_RATE_BB -> stat.textFormat(this.bbHourlyRate, currency = null)
Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> {
val netBBPer100Hands = Stat.netBBPer100Hands(this.bbNet, this.estimatedHands)
if (netBBPer100Hands != null) {
return stat.format(this.estimatedHands, currency = null)
return stat.textFormat(this.estimatedHands, currency = null)
} else {
return TextFormat(NULL_TEXT)
}

@ -8,21 +8,21 @@ import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentFeatureRow
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentFeatureRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable,
CountableUsage {
open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, UsageCountable {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
@ -53,17 +53,21 @@ open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresent
}
override fun adapterRows(): List<RowRepresentable>? {
return TournamentFeature.rowRepresentation
return rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.stringForRow(row)
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf(
"defaultValue" to this.name))
}

@ -13,14 +13,16 @@ import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentNameRow
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentNameRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource,
RowUpdatable, RowRepresentable {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
@ -50,17 +52,21 @@ open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentabl
}
override fun adapterRows(): List<RowRepresentable>? {
return TournamentName.rowRepresentation
return rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.stringForRow(row)
else -> return super.charSequenceForRow(row, context,0)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf("defaultValue" to this.name))
}

@ -1,7 +1,6 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import com.github.mikephil.charting.data.Entry
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
@ -12,39 +11,36 @@ import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
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.ui.graph.Graph
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.TransactionPropertiesRow
import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.extensions.findById
import java.text.DateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.abs
open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, TimeFilterable,
open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageable, StaticRowRepresentableDataSource, TimeFilterable,
Filterable, DatedBankrollGraphEntry {
companion object {
fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double): 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())
transaction.date = date ?: Date()
transaction.amount = amount
transaction.type = type
transaction.bankroll = bankroll
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 {
val rows = ArrayList<RowRepresentable>()
rows.addAll(TransactionRow.values())
rows
return transaction
}
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
@ -56,7 +52,10 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
QueryCondition.AnyDayOfWeek::class.java, QueryCondition.IsWeekEnd::class.java, QueryCondition.IsWeekDay::class.java -> "dayOfWeek"
QueryCondition.AnyMonthOfYear::class.java -> "month"
QueryCondition.AnyYear::class.java -> "year"
QueryCondition.PastDay::class.java, QueryCondition.IsToday::class.java, QueryCondition.WasYesterday::class.java, QueryCondition.WasTodayAndYesterday::class.java, QueryCondition.DuringThisYear::class.java, QueryCondition.DuringThisMonth::class.java, QueryCondition.DuringThisWeek::class.java -> "date"
QueryCondition.PastDay::class.java, QueryCondition.IsToday::class.java,
QueryCondition.WasYesterday::class.java, QueryCondition.WasTodayAndYesterday::class.java,
QueryCondition.DuringThisYear::class.java, QueryCondition.DuringThisMonth::class.java,
QueryCondition.DuringThisWeek::class.java, QueryCondition.DateNotNull::class.java -> "date"
else -> null
}
}
@ -74,6 +73,13 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
// The amount of the transaction
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
override var date: Date = Date()
@ -88,6 +94,12 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
// A user comment
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
override var dayOfWeek: Int? = null
override var month: Int? = null
@ -97,18 +109,34 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
@Ignore
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) {
when (row) {
TransactionRow.BANKROLL -> bankroll = value as Bankroll?
TransactionRow.TYPE -> type = value as TransactionType?
TransactionRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionRow.COMMENT -> comment = value as String? ?: ""
TransactionRow.DATE -> date = value as Date? ?: Date()
TransactionPropertiesRow.BANKROLL -> bankroll = value as Bankroll?
TransactionPropertiesRow.TYPE -> type = value as TransactionType?
TransactionPropertiesRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionPropertiesRow.COMMENT -> comment = value as String? ?: ""
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>? {
return rowRepresentation
return rowRepresentation()
}
override fun isValidForSave(): Boolean {
@ -144,6 +172,24 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
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
@Ignore
@ -154,22 +200,21 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
}
override fun formattedValue(stat: Stat): TextFormat {
return stat.format(this.amount, currency = this.bankroll?.utilCurrency)
return stat.textFormat(this.amount, currency = this.bankroll?.utilCurrency)
}
override fun legendValues(
stat: Stat,
entry: Entry,
style: GraphFragment.Style,
total: Double,
style: Graph.Style,
groupName: String,
context: Context
): LegendContent {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
val totalStatValue = stat.textFormat(total, currency = null)
val leftName = context.getString(R.string.amount)
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.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionTypeRow
import net.pokeranalytics.android.ui.view.RowUpdatable
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.IntSearchable
import java.util.*
import kotlin.collections.ArrayList
open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable,
CountableUsage {
open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource,
UsageCountable {
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),
BONUS(2, 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> {
@ -47,6 +49,8 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
BONUS -> R.string.bonus
STACKING_INCOMING -> R.string.stacking_incoming
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 rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(TransactionTypeRow.values())
rows.addAll(TransactionTypePropertiesRow.values())
rows
}
@ -68,6 +72,22 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
throw PAIllegalStateException("Transaction type ${value.name} should exist in database!")
}
fun getOrCreate(realm: Realm, value: Value, context: Context): TransactionType {
return getOrCreate(realm, value.localizedTitle(context), value.additive)
}
fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst()
return if (type != null) {
type
} else {
val transactionType = TransactionType()
transactionType.name = name
transactionType.additive = additive
realm.copyToRealm(transactionType)
}
}
}
@Ignore
@ -106,28 +126,32 @@ open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentab
return rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
SimpleRow.NAME -> this.name
else -> return super.stringForRow(row)
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun boolForRow(row: RowRepresentable): Boolean {
return when (row) {
TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive
TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive
else -> super.boolForRow(row)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf("defaultValue" to this.name))
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
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) }
}
}

@ -0,0 +1,252 @@
package net.pokeranalytics.android.model.realm.handhistory
import io.realm.RealmObject
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.formatted
/***
* An extension to transform a list of ComputedAction into
* a more compact and read-friendly list of ActionReadRow
*/
fun List<Action>.compact(positions: LinkedHashSet<Position>, heroIndex: Int?): List<ActionReadRow> {
val rows = mutableListOf<ActionReadRow>()
this.forEach {
if (it.type == Action.Type.FOLD && rows.lastOrNull()?.action == Action.Type.FOLD) {
rows.lastOrNull()?.positions?.add(positions.elementAt(it.position))
} else {
rows.add(it.toReadRow(positions, heroIndex, null)) // TODO stack. The method is used for text export only atm
}
}
return rows
}
fun Action.toReadRow(positions: LinkedHashSet<Position>, heroIndex: Int?, stack: Double?): ActionReadRow {
val pos = positions.elementAt(this.position)
val isHero = (heroIndex == this.position)
var amount = this.amount
if (this.type?.isCall == true) {
amount = this.effectiveAmount
}
return ActionReadRow(mutableListOf(pos), this.position, this.type, amount, stack, isHero)
}
open class Action : RealmObject() {
enum class Type(override var resId: Int) : RowRepresentable {
POST_SB(R.string.posts_sb),
POST_BB(R.string.post_bb),
STRADDLE(R.string.straddle),
FOLD(R.string.fold),
CHECK(R.string.check),
CALL(R.string.call),
BET(R.string.bet),
POT(R.string.pot),
RAISE(R.string.raise),
UNDEFINED_ALLIN(R.string.allin),
CALL_ALLIN(R.string.callin),
BET_ALLIN(R.string.ballin),
RAISE_ALLIN(R.string.rallin);
val isBlind: Boolean
get() {
return when (this) {
POST_SB, POST_BB -> true
else -> false
}
}
val isSignificant: Boolean
get() {
return when (this) {
POST_SB, POST_BB, STRADDLE, BET, POT, RAISE, BET_ALLIN, RAISE_ALLIN -> true
UNDEFINED_ALLIN -> throw PAIllegalStateException("Can't ask for UNDEFINED_ALLIN")
else -> false
}
}
/***
* Tells if the action pulls the player out from any new decision
*/
val isPullOut: Boolean
get() {
return when (this) {
FOLD, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN, UNDEFINED_ALLIN -> true
else -> false
}
}
/***
* Returns if the action is passive
*/
val isPassive: Boolean
get() {
return when (this) {
FOLD, CHECK -> true
else -> false
}
}
val isCall: Boolean
get() {
return when (this) {
CALL, CALL_ALLIN -> true
else -> false
}
}
val isAllin: Boolean
get() {
return when (this) {
UNDEFINED_ALLIN, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN -> true
else -> false
}
}
val color: Int
get() {
return when (this) {
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.color.kaki_lighter
FOLD -> R.color.red
else -> R.color.green
}
}
val background: Int
get() {
return when (this) {
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.drawable.rounded_kaki_medium_rect
FOLD -> R.drawable.rounded_red_rect
else -> R.drawable.rounded_green_rect
}
}
override val viewType: Int = RowViewType.TITLE_GRID.ordinal
companion object {
val defaultTypes: List<Type> by lazy {
listOf(FOLD, CHECK, BET, POT, CALL, RAISE, UNDEFINED_ALLIN)
}
}
}
/***
* The street of the action
*/
private var streetIdentifier: Int = 0
var street: Street
set(value) {
this.streetIdentifier = value.ordinal
}
get() {
return streetIdentifier.let { Street.values()[it] }
}
/***
* The index of the action
*/
var index: Int = 0
/***
* The position of the user making the action
*/
var position: Int = 0
/***
* The type of action: check, fold, raise...
*/
private var typeIdentifier: Int? = null
var type: Type?
set(value) {
this.typeIdentifier = value?.ordinal
}
get() {
return typeIdentifier?.let { Type.values()[it] }
}
/***
* The amount linked for a bet, raise...
*/
var amount: Double? = null
set(value) {
field = value
// Timber.d("/// set value = $value")
}
var effectiveAmount: Double = 0.0
var positionRemainingStack: Double? = null
val isActionSignificant: Boolean
get() {
return this.type?.isSignificant ?: false
}
val formattedAmount: String?
get() {
val amount = when (this.type) {
Type.CALL, Type.CALL_ALLIN -> this.effectiveAmount
else -> this.amount
}
return amount?.formatted
}
fun toggleType(remainingStack: Double) {
when (this.type) {
Type.BET -> {
if (remainingStack == 0.0) {
this.type = Type.BET_ALLIN
}
}
Type.BET_ALLIN -> {
if (remainingStack > 0.0) {
this.type = Type.BET
}
}
Type.RAISE -> {
if (remainingStack == 0.0) {
this.type = Type.RAISE_ALLIN
}
}
Type.RAISE_ALLIN -> {
if (remainingStack > 0.0) {
this.type = Type.RAISE
}
}
Type.CALL -> {
if (remainingStack == 0.0) {
this.type = Type.CALL_ALLIN
}
}
Type.CALL_ALLIN -> {
if (remainingStack > 0.0) {
this.type = Type.CALL
}
}
else -> {}
}
}
val displayedAmount: Double?
get() {
if (this.type?.isCall == true) {
return this.effectiveAmount
}
return this.amount
}
}

@ -0,0 +1,292 @@
package net.pokeranalytics.android.model.realm.handhistory
import android.content.Context
import android.text.SpannableString
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatTextView
import io.realm.RealmObject
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import timber.log.Timber
interface CardProperty
/***
* Returns a CharSequence containing each card representation
*/
fun List<Card>.formatted(context: Context) : CharSequence? {
var span: CharSequence? = null
this.forEach {
val formatted = it.formatted(context)
span = if (span == null) {
formatted
} else {
TextUtils.concat(span, formatted)
}
}
return span
}
open class Card : RealmObject() {
companion object {
fun newInstance(value: Int? = null, suit: Suit? = null, index: Int = 0) : Card {
val card = Card()
value?.let { card.value = it }
suit?.let { card.suit = it}
card.index = index
return card
}
}
/***
* The Value of a Card
* Can represent all values from Deuce to Ace, or a blank
*/
class Value(var value: Int?) : CardProperty, RowRepresentable {
companion object {
val undefined: Value = Value(null)
val values: List<Value> by lazy {
val v = mutableListOf(Value(null)) // null for x
(2..14).forEach { v.add(Value(it)) }
v
}
fun format(value: Int?) : String {
return when(value) {
in 2..9 -> "$value"
10 -> "T"
11 -> "J"
12 -> "Q"
13 -> "K"
14 -> "A"
else -> "X"
}
}
}
override fun getDisplayName(context: Context): String {
return this.formatted
}
val formatted: String
get() { return format(this.value) }
override val viewType: Int = RowViewType.TITLE_GRID.ordinal
}
enum class Suit(val value: String) : CardProperty, RowRepresentable {
UNDEFINED("x"),
SPADES(""),
HEART(""),
DIAMOND(""),
CLOVER("");
companion object {
val baseSuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER)
}
val displaySuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED)
}
fun format(suit: Suit?) : String {
return suit?.value ?: UNDEFINED.value
}
fun color(suit: Suit?) : Int {
return suit?.color ?: R.color.white
}
fun valueOf(suit: Int): Suit {
return when (suit) {
0 -> SPADES
1 -> DIAMOND
2 -> CLOVER
3 -> HEART
else -> throw PAIllegalStateException("unknow value: $suit")
}
}
}
val color: Int
get() {
return when (this) {
UNDEFINED -> R.color.grey
SPADES -> R.color.black
HEART -> R.color.red
DIAMOND -> R.color.blue
CLOVER -> R.color.clover
}
}
override fun getDisplayName(context: Context): String {
return this.value
}
override val viewType: Int = RowViewType.TITLE_GRID.ordinal
val evaluatorSuit: Int?
get() {
return when (this) {
SPADES -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.SPADES
CLOVER -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.CLUBS
DIAMOND -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.DIAMONDS
HEART -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.HEARTS
else -> null
}
}
}
/***
* The card value: 2..A
* can be undefined
*/
var value: Int? = null
/***
* Returns the formatted value of the card
*/
val formattedValue: String
get() { return Value.format(this.value) }
/***
* The card suit identifier: heart, spades...
*/
var suitIdentifier: Int? = null
/***
* Returns the suit of the card
*/
var suit: Suit?
get() {
return this.suitIdentifier?.let { Suit.values()[it] }
}
set(value) {
this.suitIdentifier = value?.ordinal
}
/***
* The card index in a list
*/
var index: Int = 0
/***
* Returns the street where the card belongs based on its [index]
*/
val street: Street
get() {
return when (this.index) {
in 0..2 -> Street.FLOP
3 -> Street.TURN
4 -> Street.RIVER
else -> throw PAIllegalStateException("Card doesn't belong to any street")
}
}
/***
* Formats the Card into a Spannable String with the appropriate color for the card suit
* [lineBreak] adds "\n" between the value and the suit
*/
fun formatted(context: Context, lineBreak: Boolean = false) : SpannableString {
val suit = Suit.format(this.suit)
val spannable = if (lineBreak) {
SpannableString(this.formattedValue + "\n" + suit)
} else {
SpannableString(this.formattedValue + suit)
}
val suitStart = spannable.indexOf(suit)
spannable.setSpan(
ForegroundColorSpan(context.getColor(Suit.color(this.suit))),
suitStart,
spannable.length,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
)
// Timber.d("card format = $spannable")
return spannable
}
/***
* Returns a view showing the card [value] and [suit]
*/
fun view(context: Context, layoutInflater: LayoutInflater, root: ViewGroup, blank: Boolean = false): View {
val textView = layoutInflater.inflate(R.layout.view_card, root, false) as AppCompatTextView
if (blank) {
textView.text = "\n"
} else {
textView.text = this.formatted(context, true)
}
return textView
}
fun toEvaluatorCard():net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card? {
val rank = this.value
val suit = this.suit?.evaluatorSuit
return if (rank != null && suit != null) {
net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card(rank - 2, suit)
} else {
null
}
}
fun copy(): Card {
val card = Card()
card.value = this.value
card.suit = this.suit
return card
}
val isWildCard: Boolean
get() {
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
}

@ -0,0 +1,775 @@
package net.pokeranalytics.android.model.realm.handhistory
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatTextView
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Session
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.CardHolder
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.addLineReturn
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.fullDate
import timber.log.Timber
import java.util.*
import kotlin.math.max
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean)
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable,
CardHolder, Comparator<PositionAmount>, StakesHolder {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@Ignore
override val realmObjectClass: Class<out Identifiable> = HandHistory::class.java
/***
* The date of the hand history
*/
var date: Date = Date()
set(value) {
field = value
this.updateTimeParameter(value)
}
init {
this.date = Date() // force the custom setter call
}
/***
* The session whose hand was played
*/
var session: Session? = null
/***
* The small blind
*/
var oldSmallBlind: Double? = null
set(value) {
field = value
// if (this.bigBlind == null && value != null) {
// this.bigBlind = value * 2
// }
}
/***
* The big blind
*/
var oldBigBlind: Double? = null
set(value) {
field = value
// if (this.smallBlind == null && value != null) {
// this.smallBlind = value / 2
// }
}
/***
* Big blind ante
*/
var bigBlindAnte: Boolean = false
/***
* The ante
*/
override var ante: Double? = 0.0
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
/***
* The blinds
*/
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
*/
var numberOfPlayers: Int = 9
/***
* Number of players in the hand
*/
var comment: String? = null
/***
* The position index of the hero
*/
var heroIndex: Int? = null
/***
* Indicates if the hero wins the hand
*/
var winnerPots: RealmList<WonPot> = RealmList()
/***
* The board
*/
var board: RealmList<Card> = RealmList()
/***
* The players actions
*/
var actions: RealmList<Action> = RealmList()
/***
* A list of players initial data: user, position, hand, stack
*/
var playerSetups: RealmList<PlayerSetup> = RealmList()
// Timed interface
override var dayOfWeek: Int? = null
override var month: Int? = null
override var year: Int? = null
override var dayOfMonth: Int? = null
/***
* Returns the indexes of all players
*/
val positionIndexes: IntRange
get() { return (0 until this.numberOfPlayers) }
// Deletable
override fun isValidForDelete(realm: Realm): Boolean {
return true
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun deleteDependencies(realm: Realm) {
this.board.deleteAllFromRealm()
this.playerSetups.deleteAllFromRealm()
this.actions.deleteAllFromRealm()
}
/***
* Configures a hand history with a [handSetup]
*/
fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it }
handSetup.blinds?.let { this.blinds = it }
this.session = handSetup.session
this.date = this.session?.handHistoryAutomaticDate ?: Date()
this.createActions(handSetup)
}
/***
* Creates the initial actions of the hand history
*/
private fun createActions(handSetup: HandSetup) {
this.actions.clear()
var blindValues = this.blindValues
if (blindValues.isNotEmpty()) {
blindValues.forEachIndexed { index, blind ->
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)
handSetup.straddlePositions.forEachIndexed { index, position -> // position are sorted here
val positionIndex = positions.indexOf(position)
val amount = if (index < blindValues.size) { blindValues[index] } else null
this.addAction(positionIndex, Action.Type.STRADDLE, amount)
}
// var lastStraddler: Int? = null
// val totalActions = this.actions.size
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions
// for (i in this.positionIndexes - 1) { // we don't add the BB / straddler by default in case of a walk
// this.addAction((startingPosition + i) % this.numberOfPlayers)
// }
}
/***
* Adds an action with the given [position], [type] and [amount] to the actions list
*/
private fun addAction(position: Int, type: Action.Type? = null, amount: Double? = null) {
val action = Action()
action.index = this.actions.size
action.position = position
action.type = type
action.amount = amount
action.effectiveAmount = amount ?: 0.0
this.actions.add(action)
}
/***
* Returns the board cards for a given [street]
*/
fun cardsForStreet(street: Street): MutableList<Card> {
return this.board.sortedBy { it.index }.take(street.totalBoardCards).toMutableList()
}
/***
* Returns the optional PlayerSetup object at the [position]
*/
fun playerSetupForPosition(position: Int): PlayerSetup? {
return this.playerSetups.firstOrNull { it.position == position }
}
override val cards: RealmList<Card>
get() { return this.board }
/***
* Returns the ante sum
*/
val anteSum: Double
get() {
return if (bigBlindAnte) {
this.biggestBet ?: 0.0
} else {
(this.ante ?: 0.0) * this.numberOfPlayers
}
}
/***
* Returns the sorted list of actions by index
*/
private val sortedActions: List<Action>
get() {
return this.actions.sortedBy { it.index }
}
/***
* Returns the list of undefined positions,
* meaning the positions where no PlayerSetup has been created
*/
fun undefinedPositions(): List<Position> {
val positions = Position.positionsPerPlayers(this.numberOfPlayers)
val copy = positions.clone() as LinkedHashSet<Position>
this.playerSetups.forEach {
copy.remove(positions.elementAt(it.position))
}
return copy.toList()
}
/***
* Creates and affect a PlayerSetup at the given [positionIndex]
*/
fun createPlayerSetup(positionIndex: Int): PlayerSetup {
val playerSetup = if (this.realm != null) {
this.realm.createObject(PlayerSetup::class.java) }
else {
PlayerSetup()
}
playerSetup.position = positionIndex
this.playerSetups.add(playerSetup)
return playerSetup
}
/***
* Returns the pot size at the start of the given [street]
*/
fun potSizeForStreet(street: Street): Double {
val sortedActions = this.sortedActions
val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index
?: sortedActions.size
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumOf { it.effectiveAmount }
}
@Ignore
override val viewType: Int = RowViewType.HAND_HISTORY.ordinal
override fun localizedString(context: Context): CharSequence {
val positions = Position.positionsPerPlayers(this.numberOfPlayers)
var string = ""
// Settings
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}"
val firstLineComponents = mutableListOf(this.date.fullDate(), players)
this.blinds?.let { firstLineComponents.add(it) }
// 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}")
}
}
string = string.plus(firstLineComponents.joinToString(" - "))
string = string.addLineReturn(2)
// Comment
this.comment?.let { comment ->
string = string.plus(comment)
string = string.addLineReturn(2)
}
// Players
this.playerSetups.sortedBy { it.position }.forEach {
string = string.plus(localizedPlayerSetup(it, positions, context))
string = string.addLineReturn()
}
// Actions per street
val sortedActions = this.actions.sortedBy { it.index }
// Remove SUMMARY
Street.values().dropLast(1).forEach { street ->
string = string.addLineReturn(2)
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex)
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 potSize = this.potSizeForStreet(street)
if (potSize > 0) {
streetItems.add("(" + potSize.formatted + ")")
}
string = string.plus(streetItems.joinToString(" "))
if (streetCards.isNotEmpty()) {
string = string.addLineReturn()
string = string.plus(streetCards.formatted(context) ?: "")
}
string = string.addLineReturn()
string = string.plus("-----------")
string = string.addLineReturn()
streetActions.forEach { action ->
string = string.plus(localizedAction(action, context))
string = string.addLineReturn()
}
}
}
return string
}
fun anteForPosition(position: Position): Double {
return if (this.bigBlindAnte) {
if (position == Position.BB) {
this.biggestBet ?: 0.0
} else {
0.0
}
} else {
this.ante ?: 0.0
}
}
/***
* Returns a string representation of the [playerSetup]
*/
private fun localizedPlayerSetup(playerSetup: PlayerSetup, positions: LinkedHashSet<Position>, context: Context): String {
val playerItems = mutableListOf(positions.elementAt(playerSetup.position).value)
val isHero = (playerSetup.position == this.heroIndex)
if (isHero) {
val heroString = context.getString(R.string.hero)
playerItems.add("- $heroString")
}
if (playerSetup.cards.isNotEmpty()) {
playerItems.add("[${playerSetup.cards.formatted(context)}]")
}
playerSetup.stack?.let { stack ->
playerItems.add("- $stack")
}
return playerItems.joinToString(" ")
}
/***
* Returns a string representation of the [actionReadRow]
*/
private fun localizedAction(actionReadRow: ActionReadRow, context: Context): String {
val formattedPositions = actionReadRow.positions.joinToString(", ") { it.value }
val actionItems = mutableListOf(formattedPositions)
actionReadRow.action?.let { type ->
actionItems.add(context.getString(type.resId))
}
actionReadRow.amount?.let { amount ->
actionItems.add(amount.formatted)
}
return actionItems.joinToString(" ")
}
/***
* Returns if the hero has won the hand, or part of the pot
*/
val heroWins: Boolean?
get() {
return this.heroIndex?.let { heroIndex ->
this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
} ?: run {
null
}
}
/***
* Creates a list of cards for the hand history to give a representation of the hand
* We will try to add a minimum of 5 cards using by priority:
* - the hero hand
* - the opponents hands
* - the board
*/
fun cardViews(context: Context, viewGroup: ViewGroup): List<View> {
val views = mutableListOf<View>()
val layoutInflater = LayoutInflater.from(context)
// Create all the possible cards list: hero, opponents, board
val cardSets = mutableListOf<List<Card>>()
// Hero
this.heroIndex?.let { hIndex ->
this.playerSetupForPosition(hIndex)?.cards?.let {
cardSets.add(it)
}
}
// Opponents
this.playerSetups.filter { it.cards.isNotEmpty() && it.position != this.heroIndex }.forEach {
cardSets.add(it.cards)
}
// Board
cardSets.add(this.board)
// Try to add cards but not too much
while (views.size < 5 && cardSets.isNotEmpty()) {
val cardSet = cardSets.removeAt(0)
if (views.isNotEmpty() && cardSet.isNotEmpty()) { // add separator with previous set of cards
val view = layoutInflater.inflate(R.layout.view_card_separator, viewGroup, false) as AppCompatTextView
views.add(view)
}
cardSet.forEach { views.add(it.view(context, layoutInflater, viewGroup)) }
}
// Add 5 blank cards if no card has been added
if (views.isEmpty()) {
val blankCard = Card()
(1..5).forEach { _ ->
val view = blankCard.view(context, layoutInflater, viewGroup, true)
view.setBackgroundResource(R.drawable.rounded_kaki_medium_rect)
views.add(view)
}
}
return views
}
data class Pot(var amount: Double, var level: Double, var positions: MutableSet<Int> = mutableSetOf())
/***
* Defines which positions win the hand
*/
fun defineWinnerPositions() {
val folds = this.sortedActions.filter { it.type == Action.Type.FOLD }.map { it.position }
val activePositions = this.positionIndexes.toMutableList()
activePositions.removeAll(folds)
val wonPots = when (activePositions.size) {
0 -> listOf() // no winner, everyone has fold. Should not happen
1 -> { // One player has not fold, typically BET / FOLD
val pot = WonPot()
pot.position = activePositions.first()
pot.amount = potSizeForStreet(Street.SUMMARY)
listOf(pot)
}
else -> { // Several players remains, typically BET/FOLD or CHECKS
this.wonPots(getPots(activePositions))
}
}
this.winnerPots.clear()
this.winnerPots.addAll(wonPots)
Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map {it.position}} ")
}
/***
* Returns a list with all the different pots with the appropriate eligible players
*/
fun getPots(eligiblePositions: List<Int>): List<Pot> {
var runningPotAmount = 0.0
val pots = mutableListOf<Pot>()
Street.values().forEach { street ->
val streetActions = this.actions.filter { it.street == street }
val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position }
if (allinPositions.isEmpty()) {
runningPotAmount += streetActions.sumOf { it.effectiveAmount }
} else {
val amountsPerPosition = mutableListOf<PositionAmount>()
// get all committed amounts for the street by player, by allin
this.positionIndexes.map { position ->
val playerActions = streetActions.filter { it.position == position }
val sum = playerActions.sumOf { it.effectiveAmount }
amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position)))
}
amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence
// for each player
val streetPots = mutableListOf<Pot>()
amountsPerPosition.forEach { positionAmount ->
val amount = positionAmount.amount
val position = positionAmount.position
var rest = amount
var lastPotLevel = 0.0
// put invested amount in smaller pots
streetPots.forEach { pot ->
val added = pot.level - lastPotLevel
pot.amount += added
if (eligiblePositions.contains(position)) {
pot.positions.add(position)
}
rest -= added
lastPotLevel = pot.level
}
// Adds remaining chips to the running Pot
runningPotAmount += rest
// If the player is allin, create a new pot for the relevant amount
val isAllin = allinPositions.contains(position)
if (isAllin) {
streetPots.add(Pot(runningPotAmount, amount, mutableSetOf(position)))
runningPotAmount = 0.0
}
}
pots.addAll(streetPots)
}
}
// Create a pot with the remaining chips
if (runningPotAmount > 0.0) {
pots.add(Pot(runningPotAmount, 0.0, eligiblePositions.toMutableSet()))
}
return pots
}
private fun wonPots(pots: List<Pot>): Collection<WonPot> {
val wonPots = hashMapOf<Int, WonPot>()
pots.forEach { pot ->
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
}
}
}
return wonPots.values
}
/***
* Compares the hands of the players at the given [activePositions]
* Returns the list of winning hands by position
*/
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
val results = noWildCardHands.map { cards ->
cards?.let { hand ->
EvaluatorBridge.playerHand(hand, noWildCardBoard)
} ?: run {
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)
return results.minOrNull()?.let { best ->
val winners = mutableListOf<Int>()
results.forEachIndexed { index, i ->
if (i == best && i != Int.MAX_VALUE) {
winners.add(activePositions[index])
}
}
winners
} ?: run {
listOf<Int>() // type needed for build
}
}
/***
* return a negative integer, zero, or a positive integer as the
* first argument is less than, equal to, or greater than the
* second.
*/
override fun compare(o1: PositionAmount, o2: PositionAmount): Int {
return if (o1.amount == o2.amount) {
if (o1.isAllin) -1 else 1
} else {
(o1.amount - o2.amount).toInt()
}
}
val maxPlayerCards: Int
get() {
var max = 0
this.playerSetups.forEach {
max = max(it.cards.size, max)
}
return max
}
val usesWildcards: Boolean
get() {
val boardHasWildCard = this.cards.any { it.isWildCard }
val playerCardHasWildCard = this.playerSetups.any { it.cards.any { it.isWildCard } }
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,31 @@
package net.pokeranalytics.android.model.realm.handhistory
import io.realm.RealmList
import io.realm.RealmObject
import net.pokeranalytics.android.model.realm.Player
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder
import net.pokeranalytics.android.ui.view.Localizable
open class PlayerSetup : RealmObject(), CardHolder, Localizable {
/***
* The player
*/
var player: Player? = null
/***
* The position at the table
*/
var position: Int = 0
/***
* The initial stack of the player
*/
var stack: Double? = null
/***
* The cards of the player
*/
override var cards: RealmList<Card> = RealmList()
}

@ -0,0 +1,17 @@
package net.pokeranalytics.android.model.realm.handhistory
import io.realm.RealmObject
open class WonPot: RealmObject() {
/***
* The position of the player
*/
var position: Int = 0
/***
* The amount won
*/
var amount: Double = 0.0
}

@ -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 {
/**
* 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 {
val sessions = realm.where(Session::class.java)
fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int {
var sessionQuery = realm.where(Session::class.java)
.equalTo("startDate", startDate)
.equalTo("endDate", endDate)
.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 net.pokeranalytics.android.model.realm.Location
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
@ -15,7 +15,7 @@ private fun Session.parameterRepresentation(context: Context): String {
var representation = ""
this.significantFields().forEach {
representation += this.stringForRow(it, context)
representation += this.charSequenceForRow(it, context)
}
return representation
@ -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
*/
private fun Session.significantFields(): List<SessionRow> {
private fun Session.significantFields(): List<SessionPropertiesRow> {
when (this.type) {
Session.Type.TOURNAMENT.ordinal -> {
return listOf(
SessionRow.GAME,
SessionRow.INITIAL_BUY_IN,
SessionRow.BANKROLL,
SessionRow.TABLE_SIZE,
SessionRow.TOURNAMENT_NAME,
SessionRow.TOURNAMENT_TYPE
SessionPropertiesRow.GAME,
SessionPropertiesRow.INITIAL_BUY_IN,
SessionPropertiesRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE,
SessionPropertiesRow.TOURNAMENT_NAME,
SessionPropertiesRow.TOURNAMENT_TYPE
)
}
Session.Type.CASH_GAME.ordinal -> {
return listOf(
SessionRow.GAME,
SessionRow.BLINDS,
SessionRow.BANKROLL,
SessionRow.TABLE_SIZE
SessionPropertiesRow.GAME,
SessionPropertiesRow.STAKES,
SessionPropertiesRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE
)
}
}
@ -75,8 +75,7 @@ class FavoriteSessionFinder {
*/
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) {
val favoriteSession =
favoriteSession(session.type, location, session.realm, context)
val favoriteSession = favoriteSession(session.type, location, session.realm, context)
favoriteSession?.let { fav ->
@ -87,8 +86,8 @@ class FavoriteSessionFinder {
when (session.type) {
Session.Type.CASH_GAME.ordinal -> {
session.cgSmallBlind = fav.cgSmallBlind
session.cgBigBlind = fav.cgBigBlind
session.cgAnte = fav.cgAnte
session.cgBlinds = fav.cgBlinds
}
Session.Type.TOURNAMENT.ordinal -> {
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) {
values.forEach { value ->
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
if (value != TransactionType.Value.EXPENSE) {
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
}
}
}
}

@ -33,10 +33,17 @@ class SessionSetManager {
if (session.endDate == null) {
throw ModelException("End date should never be null here")
}
val endDate = session.endDate!! // tested above
val startDate = session.startDate!!
val sessionSets = this.matchingSets(session)
cleanupSessionSets(session, sessionSets)
}
private fun matchingSets(session: Session) : RealmResults<SessionSet> {
val realm = session.realm
val endDate = session.endDate!! // tested above
val startDate = session.startDate!!
val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
@ -50,12 +57,36 @@ class SessionSetManager {
.greaterThanOrEqualTo("startDate", startDate)
.lessThanOrEqualTo("endDate", endDate)
val sessionGroups = query.findAll()
return query.findAll()
}
/**
* Multiple session sets update:
* Merges or splits session sets
* Does that by deleting then recreating
*/
private fun cleanupSessionSets(session: Session, sessionSets: RealmResults<SessionSet>) {
// get all endedSessions from sets
val allImpactedSessions = mutableSetOf<Session>()
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { allImpactedSessions.addAll(it) }
}
allImpactedSessions.add(session)
// delete all sets
sessionSets.deleteAllFromRealm()
allImpactedSessions.forEach { impactedSession ->
val sets = matchingSets(impactedSession)
this.updateTimeFrames(sets, impactedSession)
}
this.updateTimeFrames(sessionGroups, session)
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
}
/**
* Update the global timeline using the impacted [sessionSets] and the updated [session]
*/

@ -5,11 +5,11 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.fragment.SubscriptionFragment
class BillingActivity : PokerAnalyticsActivity() {
class BillingActivity : BaseActivity() {
private enum class IntentKey(val keyName: String) {
SHOW_MESSAGE("showMessage"),
@ -38,15 +38,12 @@ class BillingActivity : PokerAnalyticsActivity() {
private fun initUI() {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val fragment = SubscriptionFragment()
val showSessionMessage = intent.getBooleanExtra(IntentKey.SHOW_MESSAGE.keyName, false)
val fragment = SubscriptionFragment.newInstance(showSessionMessage)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
fragment.setData(showSessionMessage)
}

@ -0,0 +1,86 @@
package net.pokeranalytics.android.ui.activity
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.ActivityColorPickerBinding
import net.pokeranalytics.android.ui.activity.components.BaseActivity
class ColorPickerActivity : BaseActivity() {
companion object {
const val INTENT_COLOR = "INTENT_COLOR"
fun newInstance(context: Context) {
val intent = Intent(context, ColorPickerActivity::class.java)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, requestCode: Int) {
val intent = Intent(fragment.requireContext(), ColorPickerActivity::class.java)
fragment.startActivityForResult(intent, requestCode)
}
}
private lateinit var binding: ActivityColorPickerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // used to fix Oreo crash
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
binding = ActivityColorPickerBinding.inflate(layoutInflater)
setContentView(binding.root)
this.title = getString(R.string.select_a_color)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
binding.color1.setOnClickListener { manageSelectedColor(it) }
binding.color2.setOnClickListener { manageSelectedColor(it) }
binding.color3.setOnClickListener { manageSelectedColor(it) }
binding.color4.setOnClickListener { manageSelectedColor(it) }
binding.color5.setOnClickListener { manageSelectedColor(it) }
binding.color6.setOnClickListener { manageSelectedColor(it) }
binding.color7.setOnClickListener { manageSelectedColor(it) }
binding.color8.setOnClickListener { manageSelectedColor(it) }
binding.color9.setOnClickListener { manageSelectedColor(it) }
}
private fun manageSelectedColor(view: View) {
val color = when(view) {
binding.color1 -> getColor(R.color.player_color_1)
binding.color2 -> getColor(R.color.player_color_2)
binding.color3 -> getColor(R.color.player_color_3)
binding.color4 -> getColor(R.color.player_color_4)
binding.color5 -> getColor(R.color.player_color_5)
binding.color6 -> getColor(R.color.player_color_6)
binding.color7 -> getColor(R.color.player_color_7)
binding.color8 -> getColor(R.color.player_color_8)
binding.color9 -> getColor(R.color.player_color_9)
else -> getColor(R.color.player_color_1)
}
val intent = Intent()
intent.putExtra(INTENT_COLOR, color)
setResult(Activity.RESULT_OK, intent)
finish()
}
}

@ -5,9 +5,9 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.activity.components.BaseActivity
class ComparisonChartActivity : PokerAnalyticsActivity() {
class ComparisonChartActivity : BaseActivity() {
companion object {
fun newInstance(context: Context) {

@ -19,17 +19,10 @@ class ComparisonReportActivity : ReportActivity() {
*/
private fun initUI() {
parameters?.let {
val report = it.report
val title = it.title
val fragmentTransaction = supportFragmentManager.beginTransaction()
val reportDetailsFragment = ComparisonReportFragment.newInstance(report, title)
fragmentTransaction.add(R.id.reportDetailsContainer, reportDetailsFragment)
fragmentTransaction.commit()
}
parameters = null
val fragmentTransaction = supportFragmentManager.beginTransaction()
val reportDetailsFragment = ComparisonReportFragment.newInstance()
fragmentTransaction.add(R.id.reportDetailsContainer, reportDetailsFragment)
fragmentTransaction.commit()
}

@ -5,9 +5,9 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.activity.components.BaseActivity
class CurrenciesActivity : PokerAnalyticsActivity() {
class CurrenciesActivity : BaseActivity() {
companion object {
fun newInstance(context: Context) {

@ -1,58 +0,0 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.activity_data_list.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.DataListFragment
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode
class DataListActivity : PokerAnalyticsActivity() {
enum class IntentKey(val keyName: String) {
DATA_TYPE("DATA_TYPE"),
LIVE_DATA_TYPE("LIVE_DATA_TYPE"),
ITEM_DELETED("ITEM_DELETED"),
SHOW_ADD_BUTTON("SHOW_ADD_BUTTON"),
}
companion object {
fun newInstance(context: Context, dataType: Int) {
context.startActivity(getIntent(context, dataType))
}
fun newSelectInstance(fragment: Fragment, dataType: Int, showAddButton: Boolean = true) {
val context = fragment.requireContext()
fragment.startActivityForResult(getIntent(context, dataType, showAddButton), FilterActivityRequestCode.SELECT_FILTER.ordinal)
}
private fun getIntent(context: Context, dataType: Int, showAddButton: Boolean = true): Intent {
val intent = Intent(context, DataListActivity::class.java)
intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType)
intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton)
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_data_list)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
val dataType = intent.getIntExtra(IntentKey.DATA_TYPE.keyName, 0)
val showAddButton = intent.getBooleanExtra(IntentKey.SHOW_ADD_BUTTON.keyName, true)
val fragment = dataListFragment as DataListFragment
fragment.setData(dataType)
fragment.updateUI(showAddButton)
}
}

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

@ -1,71 +0,0 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.FilterDetailsFragment
class FilterDetailsActivity : PokerAnalyticsActivity() {
enum class IntentKey(val keyName: String) {
FILTER_ID("FILTER_ID"),
FILTER_CATEGORY_ORDINAL("FILTER_CATEGORY_ORDINAL")
}
companion object {
/**
* Default constructor
*/
fun newInstance(context: Context, filterId: String, filterCategoryOrdinal: Int) {
val intent = Intent(context, FilterDetailsActivity::class.java)
intent.putExtra(IntentKey.FILTER_ID.keyName, filterId)
intent.putExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, filterCategoryOrdinal)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, filterId: String, filterCategoryOrdinal: Int, requestCode: Int, filter: Filter? = null) {
val intent = Intent(fragment.requireContext(), FilterDetailsActivity::class.java)
intent.putExtra(IntentKey.FILTER_ID.keyName, filterId)
intent.putExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, filterCategoryOrdinal)
fragment.startActivityForResult(intent, requestCode)
}
}
private lateinit var fragment: FilterDetailsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filter_details)
initUI()
}
override fun onBackPressed() {
fragment.onBackPressed()
}
/**
* Init UI
*/
private fun initUI() {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val filterId = intent.getStringExtra(IntentKey.FILTER_ID.keyName)
val filterCategoryOrdinal = intent.getIntExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, 0)
fragment = FilterDetailsFragment()
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
fragment.setData(filterId, filterCategoryOrdinal)
}
}

@ -1,74 +0,0 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.FiltersFragment
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode
import net.pokeranalytics.android.ui.interfaces.FilterableType
class FiltersActivity : PokerAnalyticsActivity() {
enum class IntentKey(val keyName: String) {
FILTER_ID("FILTER_ID"),
FILTERABLE_TYPE("FILTERABLE_TYPE"),
HIDE_MOST_USED_FILTERS("HIDE_MOST_USED_FILTERS"),
;
}
private lateinit var fragment: FiltersFragment
companion object {
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, filterId: String? = null, currentFilterable: FilterableType, hideMostUsedFilters: Boolean = false) {
val intent = getIntent(fragment.requireContext(), filterId, currentFilterable, hideMostUsedFilters)
fragment.startActivityForResult(intent, FilterActivityRequestCode.CREATE_FILTER.ordinal)
}
private fun getIntent(context: Context, filterId: String?, currentFilterable: FilterableType, hideMostUsedFilters: Boolean = false): Intent {
val intent = Intent(context, FiltersActivity::class.java)
intent.putExtra(IntentKey.FILTERABLE_TYPE.keyName, currentFilterable.uniqueIdentifier)
intent.putExtra(IntentKey.HIDE_MOST_USED_FILTERS.keyName, hideMostUsedFilters)
filterId?.let {
intent.putExtra(IntentKey.FILTER_ID.keyName, it)
}
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filters)
initUI()
}
override fun onBackPressed() {
fragment.onBackPressed()
}
/**
* Init UI
*/
private fun initUI() {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val filterId = intent.getStringExtra(IntentKey.FILTER_ID.keyName)
val uniqueIdentifier= intent.getIntExtra(IntentKey.FILTERABLE_TYPE.keyName, 0)
val hideMostUsedFilters = intent.getBooleanExtra(IntentKey.HIDE_MOST_USED_FILTERS.keyName, false)
val filterableType = FilterableType.valueByIdentifier(uniqueIdentifier)
fragment = FiltersFragment()
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
fragment.setData(filterId, filterableType)
fragment.updateMostUsedFiltersVisibility(!hideMostUsedFilters)
}
}

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

Loading…
Cancel
Save