Compare commits

..

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

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

@ -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,6 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
// Crashlytics
@ -17,8 +17,8 @@ repositories {
android {
compileSdkVersion 35
buildToolsVersion "30.0.3"
compileSdkVersion 32
buildToolsVersion "30.0.2"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -29,13 +29,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 145
versionName "6.0.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -88,10 +91,6 @@ android {
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
@ -112,16 +111,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 +139,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,135 @@
</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" />
android:screenOrientation="portrait" />
<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 {
@ -82,14 +83,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 +103,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,7 +1,6 @@
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
@ -10,11 +9,8 @@ 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)
@ -27,8 +23,13 @@ class CurrencyConverterApi {
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
// val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=9b56e742a75392c8aeb7"
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 }
Timber.d("Api call = $url")
val stringRequest = object : StringRequest(

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

@ -382,7 +382,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,

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

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

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

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

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

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

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

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

@ -45,46 +45,27 @@ 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 currencyCode: String = currency.currencyCode
var currencySymbol: String = currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
@ -129,6 +110,9 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row
// RowRepresentableDelegate
override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) {
val intent = Intent()
intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode)
this.activity?.setResult(Activity.RESULT_OK, intent)

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

@ -11,12 +11,9 @@ 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
@ -45,12 +42,7 @@ 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 +51,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 +68,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
}
}
@ -165,27 +156,27 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
private fun updateMainCurrency(currencyCode: String) {
val mainCurrencyCode = UserDefaults.currency.currencyCode
Preferences.getDefaultCurrency(requireContext())?.currencyCode?.let { mainCurrencyCode ->
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
if (mainCurrencyCode == currencyCode) {
return
}
showLoader(R.string.please_wait)
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ ->
hideLoader()
CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ ->
hideLoader()
val message = requireContext().getString(R.string.currency_rate_confirmation, mainCurrencyCode, currencyCode)
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,
// val message = "Please enter the $mainCurrencyCode to $currencyCode rate to apply to all your bankrolls"
showEditTextAlertDialog(requireContext(), InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL,
message = message, editTextText = apiRate?.toString()) { value ->
value.toDoubleOrNull()?.let { rate ->
updateMainCurrency(currencyCode, rate)
}
}
}
}
}
@ -208,7 +199,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
}
override fun adapterRows(): List<RowRepresentable> {
return SettingsRow.getRows()
return rowRepresentation
}
override fun charSequenceForRow(
@ -220,7 +211,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 +220,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 +236,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 +264,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 +277,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.
}
}

@ -162,7 +162,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val async = GlobalScope.async {
val s = Date()
// Timber.d(">>> start...")
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()
@ -174,7 +174,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val e = Date()
val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> ended in $duration seconds")
Timber.d(">>> ended in $duration seconds")
}
async.await()
@ -190,11 +190,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 +238,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val tSessionGroup = ComputableGroup(Query(QueryCondition.IsTournament).merge(query), tStats)
// Timber.d(">>>>> Start computations...")
Timber.d(">>>>> Start computations...")
val options = Calculator.Options()
val computedStats = mutableListOf<Stat>()

@ -30,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,10 +1,11 @@
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
@ -41,12 +42,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,6 +176,15 @@ abstract class BaseFragment : Fragment() {
alternativeLabels)
}
/***
* Returns whether the network is available or not
*/
fun isNetworkAvailable(): Boolean {
val cm = requireContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capability = cm.getNetworkCapabilities(cm.activeNetwork)
return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
}
fun showSnackBar(message: String) {
this.view?.let { view ->
val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE)

@ -4,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
@ -58,12 +57,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() {

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

@ -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,8 +60,8 @@ 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
@ -92,6 +94,19 @@ abstract class AbstractReportFragment : DataManagerFragment() {
}
val dialog = builder.create()
dialog.setOnShowListener {
nameEditText.requestFocus()
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
dialog.setOnDismissListener {
val s =
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
s?.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
dialog.show()
} ?: throw PAIllegalStateException("Activity cannot be null")

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

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

@ -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)
}
}
}
@ -218,7 +173,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou
GlobalScope.launch(Dispatchers.Main) {
delay(300)
showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = {
playerModel.deleteComment(row)
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 {

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

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

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

@ -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"),
@ -49,11 +49,7 @@ class Preferences {
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")
PATCH_RATED_AMOUNT("patchRatedAmount[new field]")
}
enum class FeedMessage {
@ -331,44 +327,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)
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"

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

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

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

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

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

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

Loading…
Cancel
Save