Compare commits

..

No commits in common. 'master' and 'sync' have entirely different histories.
master ... sync

  1. 139
      CLAUDE.md
  2. 65
      app/build.gradle
  3. 24
      app/proguard-rules.pro
  4. 4
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  5. 3
      app/src/debug/AndroidManifest.xml
  6. 115
      app/src/main/AndroidManifest.xml
  7. BIN
      app/src/main/ic_launcher-playstore.png
  8. 21
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  9. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  10. 60
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  11. 46
      app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt
  12. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  13. 32
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  14. 18
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  15. 32
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  16. 2
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  17. 10
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  18. 2
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  19. 3
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  20. 10
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  21. 13
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  22. 5
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  23. 15
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  24. 27
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  25. 1
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  26. 131
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  27. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  28. 85
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  29. 16
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  30. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  31. 13
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  32. 53
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  33. 20
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  34. 99
      app/src/main/java/net/pokeranalytics/android/ui/activity/DatabaseCopyActivity.kt
  35. 21
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  36. 15
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  37. 25
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/BaseActivity.kt
  38. 187
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt
  39. 3
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt
  40. 1
      app/src/main/java/net/pokeranalytics/android/ui/adapter/RowRepresentableAdapter.kt
  41. 326
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  42. 49
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  43. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  44. 24
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  45. 54
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  46. 126
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  47. 100
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  48. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  49. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/Top10Fragment.kt
  50. 27
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  51. 17
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt
  52. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt
  53. 14
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  54. 4
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  55. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStakesFragment.kt
  56. 27
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt
  57. 35
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  58. 35
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt
  59. 1
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt
  60. 84
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt
  61. 30
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt
  62. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/EditableDataActivity.kt
  63. 82
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt
  64. 153
      app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataViewModel.kt
  65. 39
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt
  66. 26
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt
  67. 4
      app/src/main/java/net/pokeranalytics/android/ui/modules/filter/FilterDetailsViewModel.kt
  68. 66
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/editor/EditorAdapter.kt
  69. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/editor/EditorFragment.kt
  70. 24
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/ActionList.kt
  71. 3
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/EditorViewModel.kt
  72. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/FrameManager.kt
  73. 348
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt
  74. 12
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerAnimator.kt
  75. 8
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerView.kt
  76. 7
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/TableDrawer.kt
  77. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/KeyboardAmountView.kt
  78. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionActivity.kt
  79. 16
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt
  80. 17
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/DealtHandsPerHourFragment.kt
  81. 25
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/TransactionFilterActivity.kt
  82. 132
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/TransactionFilterFragment.kt
  83. 19
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/TransactionFilterViewModel.kt
  84. 8
      app/src/main/java/net/pokeranalytics/android/ui/view/LegendView.kt
  85. 20
      app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt
  86. 61
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  87. 76
      app/src/main/java/net/pokeranalytics/android/ui/view/keyboard/StakesKeyboardView.kt
  88. 2
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/FilterItemRow.kt
  89. 1
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/FilterSectionRow.kt
  90. 29
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/PlayerPropertiesRow.kt
  91. 5
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/SettingsRow.kt
  92. 2
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/BottomSheetViewModel.kt
  93. 73
      app/src/main/java/net/pokeranalytics/android/util/BackupOperator.kt
  94. 86
      app/src/main/java/net/pokeranalytics/android/util/BackupWorker.kt
  95. 16
      app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt
  96. 1
      app/src/main/java/net/pokeranalytics/android/util/Global.kt
  97. 156
      app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt
  98. 32
      app/src/main/java/net/pokeranalytics/android/util/Language.kt
  99. 16
      app/src/main/java/net/pokeranalytics/android/util/LocationManager.kt
  100. 73
      app/src/main/java/net/pokeranalytics/android/util/Preferences.kt
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -1,14 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
// Crashlytics
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
//////////////
repositories {
maven { url 'https://jitpack.io' } // required for MPAndroidChart
@ -17,8 +15,8 @@ repositories {
android {
compileSdkVersion 35
buildToolsVersion "30.0.3"
compileSdkVersion 32
buildToolsVersion "30.0.2"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -29,13 +27,16 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
lintOptions {
disable 'MissingTranslation'
}
defaultConfig {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 35
versionCode 180
versionName "6.0.38"
targetSdkVersion 32
versionCode 144
versionName "6.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -88,10 +89,6 @@ android {
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
@ -102,8 +99,7 @@ dependencies {
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"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency
// Android
implementation 'androidx.appcompat:appcompat:1.1.0'
@ -112,16 +108,13 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
// Places
implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0'
implementation 'com.android.billingclient:billing:5.0.0'
// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -143,36 +136,16 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// 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'
implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
// Test
testImplementation 'junit:junit:4.13.2'
testImplementation 'junit:junit:4.12'
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2'

@ -64,26 +64,4 @@
-keep class com.google.j2objc.annotations.** { *; }
# 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(...);
}
-optimizations !class/unboxing/enum

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

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

@ -1,18 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
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_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" />-->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name=".PokerAnalyticsApplication"
@ -62,186 +59,130 @@
</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" />
android:screenOrientation="portrait" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
android:windowSoftInputMode="stateAlwaysHidden"/>
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:screenOrientation="portrait" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
android:launchMode="singleTop"/>
<service
android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<service android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService" android:exported="false"/>
<meta-data
android:name="preloaded_fonts"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -7,15 +7,17 @@ import com.google.firebase.FirebaseApp
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
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.*
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber
import java.util.*
@ -24,7 +26,6 @@ import java.util.*
class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object {
@ -38,9 +39,7 @@ class PokerAnalyticsApplication : Application() {
override fun onCreate() {
super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
FirebaseApp.initializeApp(this)
UserDefaults.init(this)
@ -51,7 +50,7 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(14)
.schemaVersion(13)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
@ -82,14 +81,10 @@ class PokerAnalyticsApplication : Application() {
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// Infos
val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
@ -106,7 +101,7 @@ class PokerAnalyticsApplication : Application() {
realm.close()
if (sessionsCount < 10) {
CoroutineScope(context = Dispatchers.IO).launch {
GlobalScope.launch {
FakeDataManager.createFakeSessions(500)
}
}

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

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

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

@ -1,242 +0,0 @@
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
}
}
}

@ -9,7 +9,9 @@ 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.*
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.util.extensions.startOfDay
import java.util.*
import kotlin.math.max
@ -33,8 +35,7 @@ class Calculator {
var filterId: String? = null,
private var aggregationType: AggregationType? = null,
var userGenerated: Boolean = false,
var reportSetupId: String? = null,
var includedTransactions: List<TransactionType> = listOf()
var reportSetupId: String? = null
) {
constructor(
@ -251,16 +252,8 @@ class Calculator {
// 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
}
}
results.addStat(NET_RESULT, ratedNet)
val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
results.addStat(NET_RESULT, sum)
val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
results.addStat(HANDS_PLAYED, totalHands)
@ -295,7 +288,7 @@ class Calculator {
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStat(NET_BB_PER_100_HANDS, netBB100)
}
Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi ->
Stat.returnOnInvestment(sum, totalBuyin)?.let { roi ->
results.addStat(ROI, roi)
}
@ -312,7 +305,7 @@ class Calculator {
var average = 0.0 // also used for standard deviation later
if (computables.size > 0) {
average = ratedNet / computables.size.toDouble()
average = sum / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble()
@ -382,7 +375,6 @@ 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,
@ -433,9 +425,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) {
@ -533,7 +525,7 @@ class Calculator {
var hourlyRate = 0.0
if (gHourlyDuration != null) {
hourlyRate = ratedNet / gHourlyDuration
hourlyRate = sum / gHourlyDuration
if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate)

@ -4,14 +4,15 @@ 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
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null) {
class ComputableGroup(var query: Query, var displayedStats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
@ -84,17 +85,6 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
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
*/

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

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

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

@ -17,7 +17,7 @@ sealed class PokerAnalyticsException(message: String) : Exception(message) {
// 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)
object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing")
class QueryTypeUnhandled(clazz: String) :
PokerAnalyticsException(message = "queryWith type not handled: $clazz")
class ComparisonCriteriaUnhandled(criteria: Criteria) :

@ -2,7 +2,6 @@ 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
@ -120,7 +119,7 @@ fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
.addTag(this.id)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
WorkManager.getInstance(context).enqueue(work)
}

@ -11,10 +11,6 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query {
constructor(query: Query) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) {
this.add(elements.filterNotNull())
@ -104,11 +100,6 @@ class Query {
return this
}
fun copy(): Query {
return Query(this)
}
/*
Returns the first object Id of any QueryCondition
*/
@ -119,7 +110,6 @@ class Query {
is QueryCondition.QueryDataCondition<*> -> {
c.objectId?.let { return it }
}
else -> {}
}
}
return null

@ -23,7 +23,6 @@ import net.pokeranalytics.android.ui.view.RowViewType
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.*
@ -748,16 +747,9 @@ sealed class QueryCondition : RowRepresentable {
): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
// 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")
if (BuildConfig.DEBUG) {
fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
}
fieldName ?: return realmQuery
when (this) {
@ -848,7 +840,6 @@ sealed class QueryCondition : RowRepresentable {
}
return realmQuery
}
else -> {}
}
if (this is CustomFieldRelated) {

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

@ -57,10 +57,6 @@ class Patcher {
patchZeroTable()
}
Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) {
patchRatedAmounts()
}
patchPerformances(application)
}
@ -219,16 +215,5 @@ class Patcher {
realm.close()
}
private fun patchRatedAmounts() {
val realm = Realm.getDefaultInstance()
val transactions = realm.where<Transaction>().findAll()
realm.executeTransaction {
transactions.forEach { t ->
t.computeRatedAmount()
}
}
realm.close()
}
}
}

@ -292,11 +292,11 @@ class PokerAnalyticsMigration : RealmMigration {
// Migrate to version 13
if (currentVersion == 12) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("transferRate", Double::class.java)
schema.get("Transaction")?.let { tts ->
tts.addField("transferRate", Double::class.java)
.setNullable("transferRate", true)
schema.get("Bankroll")?.let { bs ->
ts.addRealmObjectField("destination", bs)
tts.addRealmObjectField("destination", bs)
} ?: throw PAIllegalStateException("Bankroll schema not found")
}
@ -314,27 +314,6 @@ class PokerAnalyticsMigration : RealmMigration {
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 {

@ -29,7 +29,6 @@ open class FilterCondition() : RealmObject() {
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 -> {}
}
}

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

@ -105,7 +105,7 @@ open class Result : RealmObject(), Filterable {
val isPositive: Int
get() {
return if (session?.isTournament() == true) {
if ((this.cashout ?: -1.0) > 0.0) 1 else 0 // if cashout is null we want to count a negative session
if (this.cashout ?: -1.0 >= 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else {
if (this.net >= 0.0) 1 else 0
}

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

@ -73,13 +73,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
// 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()
@ -109,15 +102,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
@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) }

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

@ -3,8 +3,6 @@ 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() {
@ -27,15 +25,4 @@ open class UserConfig : RealmObject() {
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) }
}
}

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

@ -17,19 +17,15 @@ class Seed(var context:Context) : Realm.Transaction {
fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) {
values.forEach { value ->
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)
}
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)
}
}
}
}

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

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

@ -18,6 +18,7 @@ import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.fragment.ImportFragment
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.extensions.count
import timber.log.Timber
class ImportActivity : BaseActivity() {
@ -63,13 +64,11 @@ class ImportActivity : BaseActivity() {
val fragmentTransaction = supportFragmentManager.beginTransaction()
val fragment = ImportFragment()
fragment.setData(fileURI)
// val fis = contentResolver.openInputStream(fileURI)
// Timber.d("Load fragment data with: $fis")
// fis?.let {
// fragment.setData(it)
// }
val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis")
fis?.let {
fragment.setData(it)
}
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
@ -84,7 +83,7 @@ class ImportActivity : BaseActivity() {
when (requestCode) {
RequestCode.IMPORT.value -> {
if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) {
showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = {
showAlertDialog(context = this, message = R.string.unknown_import_format_popup_message, positiveAction = {
finish()
})
}

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

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

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

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

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

@ -45,47 +45,28 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
)
}
private val availableCurrencies =
Locale.getAvailableLocales()
.mapNotNull {
try {
Currency.getInstance(it)
} catch (e: Exception) {
null
}
}.toSet()
.filter { !mostUsedCurrencyCodes.contains(it.currencyCode) }
.filter {
UserDefaults.availableCurrencyLocales.any { currencyLocale ->
Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
}
}
.sortedBy { it.displayName }
.map { CurrencyRow(it) }
// private val availableCurrencies = this.systemCurrencies.filter {
// !mostUsedCurrencyCodes.contains(it.currencyCode)
// }.filter {
// UserDefaults.availableCurrencyLocales.filter { currencyLocale ->
// Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
// }.isNotEmpty()
// }.sortedBy {
// it.displayName
// }.map {
// CurrencyRow(it)
// }
private val availableCurrencies = this.systemCurrencies.filter {
!mostUsedCurrencyCodes.contains(it.currencyCode)
}.filter {
UserDefaults.availableCurrencyLocales.filter { currencyLocale ->
Currency.getInstance(currencyLocale).currencyCode == it.currencyCode
}.isNotEmpty()
}.sortedBy {
it.displayName
}.map {
CurrencyRow(it)
}
}
private class CurrencyRow(var currency: Currency) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return this.currency.getDisplayName(Locale.getDefault()).capitalize()
return currency.getDisplayName(Locale.getDefault()).capitalize()
}
var currencyCode: String = this.currency.currencyCode
var currencySymbol: String = this.currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})"
var currencyCode: String = currency.currencyCode
var currencySymbole: String = currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}

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

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

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

@ -6,22 +6,17 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.api.CurrencyConverterApi
import net.pokeranalytics.android.databinding.FragmentSettingsBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveData
@ -38,19 +33,13 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.openContactMail
import net.pokeranalytics.android.ui.extensions.openUrl
import net.pokeranalytics.android.ui.extensions.showEditTextAlertDialog
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.FileUtils
import net.pokeranalytics.android.util.Language
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.StopNotificationManager
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener
@ -59,7 +48,7 @@ import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.Date
import java.util.*
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -76,12 +65,11 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
return fragment
}
// fun rowRepresentation(context: Context): List<RowRepresentable> {
// val rows = ArrayList<RowRepresentable>()
// val hasBackupEmail = Preferences.getBackupEmail(context)?.isNotBlank() ?: false
// rows.addAll(SettingsRow.getRows(hasBackupEmail))
// return rows
// }
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(SettingsRow.getRows())
rows
}
}
@ -153,7 +141,19 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
if (resultCode == Activity.RESULT_OK) {
data?.let {
val currencyCode = data.getStringExtra(CurrenciesFragment.INTENT_CURRENCY_CODE) ?: throw PAIllegalStateException("Missing currency code")
updateMainCurrency(currencyCode)
Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(Currency::class.java).isNull("code").or().equalTo("code", UserDefaults.currency.currencyCode).findAll().forEach { currency ->
currency.rate = Currency.DEFAULT_RATE
}
realm.where(Session::class.java).isNull("bankroll.currency.code").findAll().forEach { session ->
session.bankrollHasBeenUpdated()
}
}
realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
}
}
}
@ -163,52 +163,8 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
}
}
private fun updateMainCurrency(currencyCode: String) {
val mainCurrencyCode = UserDefaults.currency.currencyCode
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ ->
hideLoader()
val message = requireContext().getString(R.string.currency_rate_confirmation, mainCurrencyCode, currencyCode)
context?.let { context ->
showEditTextAlertDialog(context, InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL,
message = message, editTextText = apiRate?.toString()) { value ->
value.toDoubleOrNull()?.let { rate ->
updateMainCurrency(currencyCode, rate)
}
}
}
}
}
private fun updateMainCurrency(currencyCode: String, rate: Double) {
Preferences.setCurrencyCode(currencyCode, requireContext())
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(Currency::class.java).findAll().forEach { currency ->
currency.rate = (currency.rate ?: 1.0) * rate
}
realm.where(Session::class.java).findAll().forEach { session ->
session.bankrollHasBeenUpdated()
}
}
realm.close()
settingsAdapterRow.refreshRow(SettingsRow.CURRENCY)
}
override fun adapterRows(): List<RowRepresentable> {
return SettingsRow.getRows()
return rowRepresentation
}
override fun charSequenceForRow(
@ -220,7 +176,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.SUBSCRIPTION -> AppGuard.subscriptionStatus(requireContext())
SettingsRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else ""
SettingsRow.CURRENCY -> UserDefaults.currency.symbol
SettingsRow.BACKUP_EMAIL -> Preferences.getBackupEmail(requireContext()) ?: ""
else -> ""
}
}
@ -230,7 +185,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
SettingsRow.STOP_NOTIFICATION -> Preferences.showStopNotifications(requireContext())
SettingsRow.SHOULD_SHOW_BLOG_TIPS -> Preferences.shouldShowBlogTips(requireContext())
SettingsRow.SHOW_INAPP_BADGES -> Preferences.showInAppBadges(requireContext())
SettingsRow.BACKUP_EMAIL -> !Preferences.hasBackupEmail(requireContext())
else -> false
}
}
@ -247,13 +201,11 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
this.openPlayStoreAccount()
}
}
SettingsRow.LANGUAGE -> this.showLanguagePopup()
SettingsRow.RATE_APP -> showReviewManager()
SettingsRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact)
SettingsRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingsRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingsRow.DEALT_HANDS_PER_HOUR -> DealtHandsPerHourActivity.newInstance(requireContext())
SettingsRow.BACKUP_EMAIL -> this.editBackupEmail()
SettingsRow.EXPORT_CSV_SESSIONS -> this.sessionsCSVExport()
SettingsRow.EXPORT_CSV_TRANSACTIONS -> this.transactionsCSVExport()
SettingsRow.FOLLOW_US -> {
@ -277,40 +229,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
}
}
private fun showLanguagePopup() {
context?.let { context ->
val languages = Language.values()
val labels = Language.values().map { it.dualNames }
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
builder.setTitle(R.string.language_popup_message)
builder.setItems(labels.toTypedArray()) { _, which ->
// the user clicked on colors[which]
val locale = languages[which]
Preferences.setLanguageCode(locale.code, context)
this.view?.let { view ->
val snackBar = Snackbar.make(view, R.string.language_should_restart_app_popup_message, Snackbar.LENGTH_INDEFINITE)
snackBar.show()
}
}
builder.show()
}
}
private fun editBackupEmail() {
context?.let { context ->
showEditTextAlertDialog(context, InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, messageResId = R.string.backup_email_title, editTextText = Preferences.getBackupEmail(context)) { value ->
Preferences.setBackupEmail(value, context)
this.settingsAdapterRow.notifyDataSetChanged()
}
}
}
private fun showReviewManager() {
val manager = ReviewManagerFactory.create(requireContext())
@ -324,8 +242,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed
}
} else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result.
}
}

@ -2,13 +2,11 @@ package net.pokeranalytics.android.ui.fragment
import android.app.Activity
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.Toolbar
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@ -21,14 +19,16 @@ import net.pokeranalytics.android.databinding.FragmentStatsBinding
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.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.model.realm.UserConfig
import net.pokeranalytics.android.ui.fragment.components.FilterableFragment
import net.pokeranalytics.android.ui.fragment.components.RealmAsyncListener
import net.pokeranalytics.android.ui.fragment.report.ComposableTableReportFragment
import net.pokeranalytics.android.ui.modules.filter.FilterActivityRequestCode
import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.modules.filter.FiltersActivity
import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity
import timber.log.Timber
import java.util.*
@ -36,8 +36,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
private lateinit var tableReportFragment: ComposableTableReportFragment
private var transactionFilterMenuItem: MenuItem? = null
companion object {
/**
@ -75,7 +73,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java)
}
private fun initUI() {
@ -86,41 +83,27 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
this.tableReportFragment = fragment
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
toolbar.menu.removeItem(R.id.menu_item_transaction_filter)
transactionFilterMenuItem = toolbar.menu?.add(0, R.id.menu_item_transaction_filter, 1, R.string.filter)
transactionFilterMenuItem?.setIcon(R.drawable.baseline_payment_24)
transactionFilterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
setTransactionFilterItemColor()
}
}
private fun setTransactionFilterItemColor() {
context?.let {
val userConfig = UserConfig.getConfiguration(getRealm())
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_item_transaction_filter -> {
showTransactionFilter()
}
}
return super.onOptionsItemSelected(item)
}
private fun showTransactionFilter() {
context?.let {
TransactionFilterActivity.newInstance(it)
}
}
// override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java)
// override fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {
// Timber.d("Entities changes, launch stats computation, size = ${results.size}")
// val cr = results as RealmResults<ComputableResult>
// cr.forEach {
// Timber.d("### buyin = ${it.ratedBuyin} ### net result = ${it.ratedNet}")
// }
// this.launchStatComputation()
// }
// override fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
// val rows: ArrayList<RowRepresentable> = ArrayList()
// report.results.forEach { result ->
// rows.add(CustomizableRowRepresentable(title = result.group.name))
// result.group.stats?.forEach { stat ->
// rows.add(StatRow(stat, result.computedStat(stat), result.group.name))
// }
// }
// return rows
// }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@ -147,10 +130,18 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
// Business
override fun asyncListenedEntityChange(realm: Realm) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchStatComputation()
setTransactionFilterItemColor()
}
// val report = createSessionGroupsAndStartCompute(realm)
// tableReportFragment.report = report
//
// GlobalScope.launch(Dispatchers.Main) {
// tableReportFragment.showResults()
// }
}
/**
@ -158,11 +149,11 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/
private fun launchStatComputation() {
CoroutineScope(coroutineContext).launch {
GlobalScope.launch(coroutineContext) {
val async = GlobalScope.async {
val s = Date()
// Timber.d(">>> start...")
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()
@ -174,7 +165,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val e = Date()
val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> ended in $duration seconds")
Timber.d(">>> ended in $duration seconds")
}
async.await()
@ -190,11 +181,8 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
*/
private fun createSessionGroupsAndStartCompute(realm: Realm): Report {
var filter: Filter? = null
context?.let { context ->
this.currentFilter(context, realm)?.let { current ->
if (current.filterableType == currentFilterable) { filter = current }
}
val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let {
if (it.filterableType == currentFilterable) { it } else { null }
}
val allStats: List<Stat> = listOf(
@ -241,7 +229,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats)
// Timber.d(">>>>> Start computations...")
Timber.d(">>>>> Start computations...")
val options = Calculator.Options()
val computedStats = mutableListOf<Stat>()
@ -250,8 +238,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
computedStats.addAll(tStats)
options.stats = computedStats
options.includedTransactions = UserConfig.getConfiguration(realm).transactionTypes(realm)
return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), options)
}

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

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

@ -1,12 +1,12 @@
package net.pokeranalytics.android.ui.fragment.components
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.BaseActivity
@ -15,7 +15,6 @@ import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheet
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.util.CrashLogging
import timber.log.Timber
import java.io.File
import java.util.*
@ -41,12 +40,6 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
v.setPadding(0, statusBarHeight, 0, 0)
insets
}
}
override fun onResume() {
@ -181,13 +174,13 @@ abstract class BaseFragment : Fragment() {
alternativeLabels)
}
fun showSnackBar(message: String) {
this.view?.let { view ->
val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE)
snackBar.show()
} ?: run {
Timber.d("No parent view for snackbar")
}
/***
* Returns whether the network is available or not
*/
fun isNetworkAvailable(): Boolean {
val cm = requireContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capability = cm.getNetworkCapabilities(cm.activeNetwork)
return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
}
}

@ -4,7 +4,6 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.view.*
import android.widget.ImageView
@ -28,7 +27,8 @@ import net.pokeranalytics.android.util.Preferences
* - Listen for INTENT_FILTER_UPDATE_FILTER_UI
* -
*/
open class FilterableFragment : RealmFragment(), FilterHandler {
open class FilterableFragment : RealmFragment(),
FilterHandler {
override var currentFilterable: FilterableType = FilterableType.ALL
set(value) {
@ -58,12 +58,11 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI), Context.RECEIVER_EXPORTED)
} else {
parentActivity?.registerReceiver(updateFilterUIBroadcast, IntentFilter(INTENT_FILTER_UPDATE_FILTER_UI))
}
parentActivity?.registerReceiver(
updateFilterUIBroadcast, IntentFilter(
INTENT_FILTER_UPDATE_FILTER_UI
)
)
}
override fun onDestroy() {
@ -84,7 +83,7 @@ open class FilterableFragment : RealmFragment(), FilterHandler {
super.onCreateOptionsMenu(menu, inflater)
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
toolbar.menu.removeItem(R.id.menu_item_filter)
filterMenuItem = toolbar.menu?.add(0, R.id.menu_item_filter, 10, R.string.filter)
filterMenuItem = toolbar.menu?.add(0, R.id.menu_item_filter, 0, R.string.filter)
filterMenuItem?.setIcon(R.drawable.ic_outline_filter_list)
filterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}

@ -47,7 +47,7 @@ class LoaderDialogFragment: DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let { bundle ->
arguments?.let {bundle ->
if (bundle.containsKey(ARGUMENT_MESSAGE_RES_ID)) {
binding.loadingMessage.text = getString(bundle.getInt(ARGUMENT_MESSAGE_RES_ID))
}

@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmResults
@ -78,6 +76,18 @@ open class RealmFragment : BaseFragment() {
this.observedRealmResults.add(results)
}
// fun listenRealmChanges(listener: RealmAsyncListener, clazz: Class<out RealmModel>) {
//
// this.changeListener = listener
//
// this.realmResults = this.realm.where(clazz).findAllAsync()
// this.realmResults?.addChangeListener { t, _ ->
// Timber.d("Realm changes: ${realmResults?.size}, $this")
// this.changeListener?.asyncListenedEntityChange(t.realm)
// }
//
// }
override fun onDestroyView() {
super.onDestroyView()

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

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

@ -3,9 +3,11 @@ package net.pokeranalytics.android.ui.fragment.report
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
@ -58,17 +60,17 @@ abstract class AbstractReportFragment : DataManagerFragment() {
override fun saveData() {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
activity?.let {
val builder = AlertDialog.Builder(it)
// Get the layout inflater
val inflater = requireActivity().layoutInflater
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
val view = inflater.inflate(R.layout.dialog_edit_text, null)
val view = inflater.inflate(net.pokeranalytics.android.R.layout.dialog_edit_text, null)
val nameEditText =
view.findViewById<EditText>(R.id.reportName)
view.findViewById<EditText>(net.pokeranalytics.android.R.id.reportName)
nameEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
this.model.primaryKey?.let { id ->
@ -79,7 +81,7 @@ abstract class AbstractReportFragment : DataManagerFragment() {
builder.setView(view)
// Add action buttons
.setPositiveButton(R.string.save) { dialog, _ ->
.setPositiveButton(net.pokeranalytics.android.R.string.save) { dialog, _ ->
try {
saveReport(nameEditText.text.toString())
dialog.dismiss()
@ -87,11 +89,24 @@ abstract class AbstractReportFragment : DataManagerFragment() {
Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
.setNegativeButton(net.pokeranalytics.android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}
val dialog = builder.create()
dialog.setOnShowListener {
nameEditText.requestFocus()
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
dialog.setOnDismissListener {
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
dialog.show()
} ?: throw PAIllegalStateException("Activity cannot be null")

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

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

@ -177,7 +177,6 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource
when (model.sessionTypeCondition) {
QueryCondition.IsCash -> query.add(QueryCondition.IsCash)
QueryCondition.IsTournament -> query.add(QueryCondition.IsTournament)
else -> {}
}
val requiredStats: List<Stat> = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY)

@ -1,10 +1,7 @@
package net.pokeranalytics.android.ui.modules.calendar
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import io.realm.Realm
@ -24,7 +21,6 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.UserConfig
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
@ -33,7 +29,6 @@ import net.pokeranalytics.android.ui.extensions.hideWithAnimation
import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.components.RealmAsyncListener
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity
import net.pokeranalytics.android.ui.view.CalendarTabs
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
@ -41,7 +36,6 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.util.*
import kotlin.collections.set
class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentableDataSource,
@ -95,6 +89,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
setHasOptionsMenu(true)
_binding = FragmentCalendarBinding.inflate(inflater, container, false)
return binding.root
}
@ -106,47 +101,19 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
addRealmChangeListener(this, UserConfig::class.java)
addRealmChangeListener(this, ComputableResult::class.java)
addRealmChangeListener(this, Transaction::class.java)
}
private var transactionFilterMenuItem: MenuItem? = null
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu.clear()
inflater.inflate(R.menu.toolbar_calendar, menu)
super.onCreateOptionsMenu(menu, inflater)
view?.findViewById<Toolbar>(R.id.toolbar)?.let { toolbar ->
toolbar.menu.removeItem(R.id.menu_item_transaction_filter)
transactionFilterMenuItem = toolbar.menu?.add(0, R.id.menu_item_transaction_filter, 1, R.string.filter)
transactionFilterMenuItem?.setIcon(R.drawable.baseline_payment_24)
transactionFilterMenuItem?.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
setTransactionFilterItemColor()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_item_transaction_filter -> showTransactionFilter()
}
return super.onOptionsItemSelected(item)
}
private fun setTransactionFilterItemColor() {
context?.let {
val userConfig = UserConfig.getConfiguration(getRealm())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.transactionFilterMenuItem?.let { item ->
val color = if (userConfig.transactionTypeIds.isNotEmpty()) R.color.red else R.color.white
item.iconTintList = ColorStateList.valueOf(it.getColor(color))
}
}
}
}
private fun showTransactionFilter() {
context?.let {
TransactionFilterActivity.newInstance(it)
R.id.grid -> showGridCalendar()
}
return true
}
override fun onDestroyView() {
@ -213,6 +180,9 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
override val observedEntities: List<Class<out RealmModel>> = listOf(ComputableResult::class.java)
// override fun entitiesChanged(clazz: Class<out RealmModel>, results: RealmResults<out RealmModel>) {
// launchAsyncStatComputation()
// }
// Business
@ -313,8 +283,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
layoutManager = viewManager
adapter = calendarAdapter
}
setTransactionFilterItemColor()
}
private fun selectTimeFilter(timeFilter: TimeFilter, isChecked: Boolean) {
@ -378,14 +346,11 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Stat.STANDARD_DEVIATION_HOURLY
)
val transactionTypes = UserConfig.getConfiguration(realm).transactionTypes(realm)
// All
val allOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = Query(this.sessionTypeCondition),
includedTransactions = transactionTypes
query = Query(this.sessionTypeCondition)
)
val allReport = Calculator.computeStats(realm, options = allOptions)
this.allComputedResults = allReport.results.first()
@ -394,8 +359,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val smOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = Query(this.slidingMonthQueryCondition, this.sessionTypeCondition),
includedTransactions = transactionTypes
query = Query(this.slidingMonthQueryCondition, this.sessionTypeCondition)
)
val smReport = Calculator.computeStats(realm, options = smOptions)
@ -414,8 +378,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = query,
includedTransactions = transactionTypes
query = query
)
val report = Calculator.computeStats(realm, options = options)
report.results.forEach { computedResults ->
@ -431,7 +394,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Calendar.MONTH,
condition.listOfValues.first()
)
else -> {}
}
}
@ -444,8 +406,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val syOptions = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = Query(this.slidingYearQueryCondition, this.sessionTypeCondition),
includedTransactions = transactionTypes
query = Query(this.slidingYearQueryCondition, this.sessionTypeCondition)
)
val syReport = Calculator.computeStats(realm, options = syOptions)
@ -466,8 +427,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
val options = Calculator.Options(
progressValues = Calculator.Options.ProgressValues.STANDARD,
stats = requiredStats,
query = query,
includedTransactions = transactionTypes
query = query
)
val report = Calculator.computeStats(realm, options = options)
report.results.forEach { computedResults ->
@ -479,7 +439,6 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Calendar.YEAR,
condition.listOfValues.first()
)
else -> {}
}
}
yearlyReports[calendar.time] = computedResults
@ -492,6 +451,18 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms")
// Logs
/*
Timber.d("========== AnyYear x AnyMonthOfYear")
sortedMonthlyReports.keys.forEach {
Timber.d("$it => ${sortedMonthlyReports[it]?.computedStat(Stat.NET_RESULT)?.value}")
}
Timber.d("========== YEARLY")
sortedYearlyReports.keys.forEach {
Timber.d("$it => ${sortedYearlyReports[it]?.computedStat(Stat.NET_RESULT)?.value}")
}
*/
}
/**
@ -605,10 +576,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable
}
override fun asyncListenedEntityChange(realm: Realm) {
if (isAdded) { // Fixes: java.lang.IllegalStateException Fragment StatisticsFragment{9d3e5ec} not attached to a context.
launchAsyncStatComputation()
setTransactionFilterItemColor()
}
launchAsyncStatComputation()
}
private fun showGridCalendar() {

@ -8,14 +8,13 @@ import android.view.View
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
import net.pokeranalytics.android.R
import net.pokeranalytics.android.api.CurrencyConverterApi
import net.pokeranalytics.android.api.FreeConverterApi
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.ResultCaptureType
import net.pokeranalytics.android.ui.activity.CurrenciesActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.ui.fragment.CurrenciesFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
@ -85,8 +84,6 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
onRowValueChanged(currencyCode, BankrollPropertiesRow.CURRENCY)
if (shouldShowCurrencyRate) {
refreshRate()
} else {
this.bankroll.currency?.rate = 1.0
}
}
}
@ -140,7 +137,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence {
return when (row) {
SimpleRow.NAME -> bankroll.name.ifEmpty { NULL_TEXT }
SimpleRow.NAME -> if (bankroll.name.isNotEmpty()) bankroll.name else NULL_TEXT
BankrollPropertiesRow.CURRENCY -> {
bankroll.currency?.code?.let { code ->
Currency.getInstance(code).currencyCode
@ -266,24 +263,12 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
}
this.lastRefreshRateCall = System.currentTimeMillis()
// val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}"
val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}"
bankroll.currency?.code?.let { from ->
val to = defaultCurrency.currencyCode
CurrencyConverterApi.currencyRate(from, to, requireContext()) { rate, error ->
rate?.let {
onRowValueChanged(rate, BankrollPropertiesRow.RATE)
}
error?.localizedMessage?.let { message ->
toast(message)
}
// onRowValueChanged(rate, BankrollPropertiesRow.RATE)
isRefreshingRate = false
rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE)
}
FreeConverterApi.currencyRate(currenciesConverterValue, requireContext()) { rate ->
onRowValueChanged(rate, BankrollPropertiesRow.RATE)
isRefreshingRate = false
rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE)
}
this.isRefreshingRate = true
@ -295,6 +280,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
this.bankrollModel.selectedCaptureType.value?.let {
Preferences.setResultCaptureType(this.bankroll, it, requireContext())
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -11,7 +11,6 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
@ -37,10 +36,7 @@ import net.pokeranalytics.android.ui.helpers.DateTimePickerManager
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableDiffCallback
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.Preferences
@ -80,7 +76,7 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
override fun onResume() {
super.onResume()
// CrashLogging.log("session (id=${this.currentSession.id}): valid=${currentSession.isValid}, managed=${currentSession.isManaged}, loaded=${currentSession.isLoaded} ")
CrashLogging.log("session (id=${this.currentSession.id}): valid=${currentSession.isValid}, managed=${currentSession.isManaged}, loaded=${currentSession.isLoaded} ")
this.refreshTimer()
}
@ -130,10 +126,10 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
setDisplayHomeAsUpEnabled(true)
// val viewManager = SmoothScrollLinearLayoutManager(requireContext())
val viewManager = SmoothScrollLinearLayoutManager(requireContext())
binding.recyclerView.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(requireContext())
layoutManager = viewManager
}
binding.floatingActionButton.setOnClickListener {
@ -412,11 +408,11 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
CoroutineScope(coroutineContext).launch {
var optimalDuration: Double? = null
var optimalDuration: Double?
val cr = GlobalScope.async {
optimalDuration = CashGameOptimalDurationCalculator.start(isLive)
}
optimalDuration = CashGameOptimalDurationCalculator.start(isLive)
cr.await()
if (!isDetached) {

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

@ -1,25 +0,0 @@
package net.pokeranalytics.android.ui.modules.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.BaseActivity
class TransactionFilterActivity : BaseActivity() {
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, TransactionFilterActivity::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_transaction_filter)
}
}

@ -1,132 +0,0 @@
package net.pokeranalytics.android.ui.modules.settings
import android.content.Context
import android.os.Bundle
import android.view.*
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.databinding.FragmentTransactionFilterBinding
import net.pokeranalytics.android.model.realm.TransactionType
import net.pokeranalytics.android.model.realm.UserConfig
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
class TransactionFilterFragment : RealmFragment(), StaticRowRepresentableDataSource,
RowRepresentableDelegate {
private var _binding: FragmentTransactionFilterBinding? = null
private val binding get() = _binding!!
private lateinit var model: TransactionFilterViewModel
private lateinit var rowRepresentableAdapter: RowRepresentableAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.model = activity?.run {
ViewModelProvider(this).get(TransactionFilterViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = FragmentTransactionFilterBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
initData()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu.clear()
inflater.inflate(R.menu.toolbar_save, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save -> save()
}
return true
}
private fun initUI() {
setDisplayHomeAsUpEnabled(true)
val viewManager = LinearLayoutManager(requireContext())
this.binding.recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
}
this.rowRepresentableAdapter = RowRepresentableAdapter(this, this)
this.binding.recyclerView.adapter = rowRepresentableAdapter
}
private fun initData() {
val transactionTypes = getRealm().where<TransactionType>()
.notEqualTo("kind", TransactionType.Value.DEPOSIT.uniqueIdentifier)
.notEqualTo("kind", TransactionType.Value.WITHDRAWAL.uniqueIdentifier)
.notEqualTo("kind", TransactionType.Value.TRANSFER.uniqueIdentifier)
.notEqualTo("kind", TransactionType.Value.STACKING_INCOMING.uniqueIdentifier)
.notEqualTo("kind", TransactionType.Value.STACKING_OUTGOING.uniqueIdentifier)
.sort("name")
.findAll()
this.model.transactionTypes = transactionTypes
val userConfig = UserConfig.getConfiguration(this.getRealm())
this.model.selectedTransactionTypes = userConfig.transactionTypes(getRealm()).toMutableSet()
}
private fun save() {
getRealm().executeTransaction { realm ->
val userConfig = UserConfig.getConfiguration(realm)
userConfig.setTransactionTypeIds(this.model.selectedTransactionTypes)
realm.copyToRealmOrUpdate(userConfig)
}
this.activity?.finish()
}
override fun adapterRows(): List<RowRepresentable> {
return this.model.transactionTypes.map { TransactionTypeSwitchRow(it) }
}
override fun boolForRow(row: RowRepresentable): Boolean {
val transactionTypeRow = row as TransactionTypeSwitchRow
return this.model.selectedTransactionTypes.contains(transactionTypeRow.transactionType)
}
override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
val isChecked = value as Boolean
val transactionTypeRow = row as TransactionTypeSwitchRow
this.model.selectTransactionType(transactionTypeRow.transactionType, isChecked)
}
}
class TransactionTypeSwitchRow(val transactionType: TransactionType) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return transactionType.name
}
override val viewType: Int = RowViewType.TITLE_SWITCH.identifier
}

@ -1,19 +0,0 @@
package net.pokeranalytics.android.ui.modules.settings
import androidx.lifecycle.ViewModel
import net.pokeranalytics.android.model.realm.TransactionType
class TransactionFilterViewModel : ViewModel() {
lateinit var transactionTypes: List<TransactionType>
var selectedTransactionTypes: MutableSet<TransactionType> = mutableSetOf()
fun selectTransactionType(transactionType: TransactionType, selected: Boolean) {
when(selected) {
true -> this.selectedTransactionTypes.add(transactionType)
false -> this.selectedTransactionTypes.remove(transactionType)
}
}
}

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

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

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

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

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

@ -63,7 +63,6 @@ sealed class FilterSectionRow(override val resId: Int?) : RowRepresentable {
is CustomField -> {
return customField.name
}
else -> {}
}
return name
}

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

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

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

@ -1,73 +0,0 @@
package net.pokeranalytics.android.util
import android.content.Context
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.util.csv.DataType
import timber.log.Timber
import java.util.concurrent.TimeUnit
class BackupOperator(var context: Context) {
private var sessions: RealmResults<Session>? = null
private var transactions: RealmResults<Transaction>? = null
private var sessionsInitialized = false
private var transactionsInitialized = false
private val realm = Realm.getDefaultInstance()
init {
this.sessions = this.realm.where(Session::class.java).findAllAsync()
this.sessions?.addChangeListener { _ ->
if (this.sessionsInitialized) {
Preferences.getBackupEmail(context)?.let {
backupDataType(DataType.SESSION)
}
}
this.sessionsInitialized = true
}
this.transactions = this.realm.where(Transaction::class.java).findAllAsync()
this.transactions?.addChangeListener { _ ->
if (this.transactionsInitialized) {
Preferences.getBackupEmail(context)?.let {
backupDataType(DataType.TRANSACTION)
}
}
this.transactionsInitialized = true
}
}
private fun backupDataType(dataType: DataType) {
val data = Data.Builder()
.putInt(BackupWorker.ParamKeys.DATA.value, dataType.ordinal)
var duration = 10L
var unit = TimeUnit.HOURS
if (BuildConfig.DEBUG) {
duration = 1L
unit = TimeUnit.SECONDS
}
val backupWorker = OneTimeWorkRequestBuilder<BackupWorker>()
.setInitialDelay(duration, unit)
.setInputData(data.build())
.addTag(dataType.workId)
.build()
Timber.d(">>> create backupTask")
WorkManager.getInstance(context).enqueueUniqueWork(dataType.workId, ExistingWorkPolicy.REPLACE, backupWorker)
}
}

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

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

@ -4,4 +4,3 @@ const val NULL_TEXT: String = "--"
const val RANDOM_PLAYER: String = ""
const val FFMPEG_DESCRIPTOR_FILE = "descriptor.txt"
const val BLIND_SEPARATOR: String = "/"
const val UUID_SEPARATOR: String = ","

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

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

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

@ -33,7 +33,7 @@ class Preferences {
PATCH_SESSION_SETS("patchSessionSet"),
PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"),
// PATCH_BLINDS_FORMAT("patchBlindFormat"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions_v2"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions"),
PATCH_ZERO_TABLE("patchZeroTable"),
SHOW_STOP_NOTIFICATIONS("showStopNotifications"),
ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"),
@ -47,13 +47,8 @@ class Preferences {
PATCH_NEGATIVE_LIMITS("negativeLimits"),
PATCH_STAKES("patchStakes"),
CLEAN_BLINDS_FILTERS("deleteBlindsFilters"),
SHOW_IN_APP_BADGES("showInAppBadges"),
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"),
PATCH_RATED_AMOUNT("patchRatedAmount[new field]"),
BACKUP_EMAIL("backupEmail"),
LANGUAGE_CODE("languageCode"),
SESSIONS_BACKUP_SUCCESS("sessionsBackupSuccess"),
TRANSACTIONS_BACKUP_SUCCESS("transactionsBackupSuccess")
SHOW_INAPP_BADGES("showInAppBadges"),
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate")
}
enum class FeedMessage {
@ -106,18 +101,6 @@ class Preferences {
companion object {
// fun setStringSet(key: PreferenceKey, value: MutableSet<String>, context: Context) {
// val preferences = PreferenceManager.getDefaultSharedPreferences(context)
// val editor = preferences.edit()
// editor.putStringSet(key.identifier, value)
// editor.apply()
// }
//
// fun getStringSet(key: Keys, context: Context): MutableSet<String>? {
// val preferences = PreferenceManager.getDefaultSharedPreferences(context)
// return preferences.getStringSet(key.identifier, null)
// }
fun setString(key: PreferenceKey, value: String, context: Context) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val editor = preferences.edit()
@ -249,6 +232,14 @@ class Preferences {
}
}
// fun executeOnceInThread(key: Keys, context: Context, executable: () -> Unit) {
//
// if (!getBoolean(key, context)) {
// Thread { executable.invoke() }
// setBoolean(key, true, context)
// }
// }
fun setResultCaptureType(bankroll: Bankroll, type: ResultCaptureType, context: Context) {
val key = "${Keys.BANKROLL_RESULT_CAPTURE_TYPE}${bankroll.id}"
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -316,11 +307,11 @@ class Preferences {
}
fun showInAppBadges(context: Context): Boolean {
return getBoolean(Keys.SHOW_IN_APP_BADGES, context, true)
return getBoolean(Keys.SHOW_INAPP_BADGES, context, true)
}
fun setShowInAppBadges(context: Context, show: Boolean) {
setBoolean(Keys.SHOW_IN_APP_BADGES, show, context)
setBoolean(Keys.SHOW_INAPP_BADGES, show, context)
}
fun lastCalendarBadgeDate(context: Context): Long {
@ -331,44 +322,6 @@ class Preferences {
setLong(Keys.LAST_CALENDAR_BADGE_DATE, date, context)
}
fun setBackupEmail(email: String, context: Context) {
setString(Keys.BACKUP_EMAIL, email, context)
}
fun getBackupEmail(context: Context): String? {
return getString(Keys.BACKUP_EMAIL, context)
}
fun hasBackupEmail(context: Context): Boolean {
getString(Keys.BACKUP_EMAIL, context)?.let {
return it.isNotEmpty()
}
return false
}
fun setLanguageCode(languageCode: String, context: Context) {
setString(Keys.LANGUAGE_CODE, languageCode, context)
}
fun getLanguageCode(context: Context): String? {
return getString(Keys.LANGUAGE_CODE, context)
}
fun setTransactionsBackupSuccess(success: Boolean, context: Context) {
setBoolean(Keys.TRANSACTIONS_BACKUP_SUCCESS, success, context)
}
fun transactionsBackupSuccess(context: Context): Boolean {
return getBoolean(Keys.TRANSACTIONS_BACKUP_SUCCESS, context, true)
}
fun setSessionsBackupSuccess(success: Boolean, context: Context) {
setBoolean(Keys.SESSIONS_BACKUP_SUCCESS, success, context)
}
fun sessionsBackupSuccess(context: Context): Boolean {
return getBoolean(Keys.SESSIONS_BACKUP_SUCCESS, context, true)
}
}
}

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

Loading…
Cancel
Save