Compare commits

..

1 Commits

Author SHA1 Message Date
Laurent 7f8a6d4618 upgrade gradle and shit 2 years ago
  1. 139
      CLAUDE.md
  2. 42
      app/build.gradle
  3. 4
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  4. 82
      app/src/main/AndroidManifest.xml
  5. 1
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  6. 4
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  7. 22
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  8. 5
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  9. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  10. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  11. 21
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  12. 3
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  13. 54
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  14. 12
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  15. 8
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt
  16. 12
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/FilterableFragment.kt
  17. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/RealmFragment.kt
  18. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetStakesFragment.kt
  19. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  20. 17
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/ActionList.kt
  21. 6
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/FrameManager.kt
  22. 341
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt
  23. 1
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerAnimator.kt
  24. 8
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayerView.kt
  25. 13
      app/src/main/java/net/pokeranalytics/android/ui/modules/settings/DealtHandsPerHourFragment.kt
  26. 4
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  27. 76
      app/src/main/java/net/pokeranalytics/android/ui/view/keyboard/StakesKeyboardView.kt
  28. 17
      app/src/main/java/net/pokeranalytics/android/util/BackupOperator.kt
  29. 37
      app/src/main/java/net/pokeranalytics/android/util/BackupTask.kt
  30. 16
      app/src/main/java/net/pokeranalytics/android/util/LocationManager.kt
  31. 21
      app/src/main/java/net/pokeranalytics/android/util/Preferences.kt
  32. 3
      app/src/main/res/layout/activity_gdpr.xml
  33. 4
      app/src/main/res/layout/activity_graph.xml
  34. 1
      app/src/main/res/layout/fragment_filters.xml
  35. 7
      app/src/main/res/layout/fragment_graph.xml
  36. 17
      app/src/main/res/layout/fragment_replayer.xml
  37. 14
      app/src/main/res/layout/fragment_reports.xml
  38. 12
      build.gradle
  39. 3
      gradle.properties
  40. 1
      settings.gradle

@ -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: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android' apply plugin: 'realm-android'
// Crashlytics // Crashlytics
@ -17,7 +17,7 @@ repositories {
android { android {
compileSdkVersion 35 compileSdkVersion 33
buildToolsVersion "30.0.3" buildToolsVersion "30.0.3"
compileOptions { compileOptions {
@ -29,13 +29,16 @@ android {
jvmTarget = JavaVersion.VERSION_1_8 jvmTarget = JavaVersion.VERSION_1_8
} }
lintOptions {
disable 'MissingTranslation'
}
defaultConfig { defaultConfig {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 35 targetSdkVersion 33
versionCode 180 versionCode 165
versionName "6.0.38" versionName "6.0.22"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -89,15 +92,14 @@ android {
viewBinding true viewBinding true
} }
namespace 'net.pokeranalytics.android' namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation (project(":shared"))
// Kotlin // Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
@ -112,16 +114,13 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Places // Places
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions // Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0' implementation 'com.android.billingclient:billing:5.0.0'
// Import the BoM for the Firebase platform // Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation platform('com.google.firebase:firebase-bom:26.1.0')
@ -143,10 +142,10 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export) // ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS' implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera // Camera
def camerax_version = "1.1.0" def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}"
@ -162,17 +161,14 @@ dependencies {
// Retrofit // Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests // Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1' androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test:runner:1.6.2' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.6.1' androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
// Test // 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:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2' testImplementation 'com.android.support.test:rules:1.0.2'

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

@ -83,165 +83,137 @@
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity" android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity" android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity" android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"/>
android:exported="true"/>
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity" android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity" android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity" android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity" android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity" android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity" android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard" android:launchMode="standard"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity" android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity" android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity" android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity" android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity" android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity" android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity" android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity" android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity" android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity" android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog" android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop" android:launchMode="singleTop"/>
android:exported="true"/>
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity" android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<service <service android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService" android:exported="false"/>
android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"

@ -89,6 +89,7 @@ class PokerAnalyticsApplication : Application() {
val locale = Locale.getDefault() val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}") CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Timber.d(Greeting)
// Realm.getDefaultInstance().executeTransaction { // Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java) // it.delete(Performance::class.java)

@ -4,7 +4,6 @@ import android.content.Context
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.isNetworkAvailable import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -38,7 +37,7 @@ interface MyBackupApi {
object BackupApi { object BackupApi {
private val service = BackupService() val service = BackupService()
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send // 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 { suspend fun backupFile(context: Context, mail: String, fileName: String, fileContent: String): Boolean {
@ -61,7 +60,6 @@ object BackupApi {
true true
} catch (e: Exception) { } catch (e: Exception) {
Timber.d("!!! backup failed: ${e.message}") Timber.d("!!! backup failed: ${e.message}")
CrashLogging.logException(e)
false false
} }
} }

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

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

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

@ -479,11 +479,7 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
val heroWins: Boolean? val heroWins: Boolean?
get() { get() {
return this.heroIndex?.let { heroIndex -> return this.heroIndex?.let { heroIndex ->
this.largestWonPot?.let { pot -> heroIndex == this.largestWonPot?.position
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex } // this.winnerPots.any { it.position == heroIndex }
} ?: run { } ?: run {
null null

@ -17,10 +17,8 @@ import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.BaseActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.BackupTask
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.DataType
import net.pokeranalytics.android.util.extensions.findAll import net.pokeranalytics.android.util.extensions.findAll
import net.pokeranalytics.android.util.extensions.isSameMonth import net.pokeranalytics.android.util.extensions.isSameMonth
import java.util.* import java.util.*
@ -78,7 +76,6 @@ class HomeActivity : BaseActivity(), NewPerformanceListener {
AppGuard.requestPurchasesUpdate() AppGuard.requestPurchasesUpdate()
this.homePagerAdapter?.activityResumed() this.homePagerAdapter?.activityResumed()
lookForCalendarBadge() lookForCalendarBadge()
checkForFailedBackups()
} }
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
@ -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()
}
}
}
} }

@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.charts.LineChart
@ -103,7 +101,6 @@ class GraphFragment : RealmFragment(), OnChartValueSelectedListener {
initData() initData()
initUI() initUI()
loadGraph() loadGraph()
} }
private fun initData() { private fun initData() {

@ -1,8 +1,6 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -10,7 +8,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
@ -21,8 +18,6 @@ import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.NewPerformanceListener import net.pokeranalytics.android.calculus.NewPerformanceListener
import net.pokeranalytics.android.calculus.ReportTask
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.calcul.ReportDisplay import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.databinding.FragmentReportsBinding import net.pokeranalytics.android.databinding.FragmentReportsBinding
@ -31,7 +26,6 @@ import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.Performance import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.ReportSetup import net.pokeranalytics.android.model.realm.ReportSetup
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.activity.ReportCreationActivity
import net.pokeranalytics.android.ui.activity.components.ReportActivity import net.pokeranalytics.android.ui.activity.components.ReportActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -47,7 +41,7 @@ import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import timber.log.Timber import timber.log.Timber
import java.util.Date import java.util.*
data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) { data class ReportSection(val report: StaticReport, var performances: MutableList<PerformanceRow>) {
@ -188,24 +182,10 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
adapter = dataListAdapter adapter = dataListAdapter
} }
binding.addButton.setOnClickListener { binding.addButton.setOnClickListener {
ReportCreationActivity.newInstanceForResult(this, requireContext()) ReportCreationActivity.newInstanceForResult(this, requireContext())
} }
val sessionCount = getRealm().where(Result::class.java).count()
binding.computeButton.isVisible = adapterRows.isEmpty() && sessionCount > 5
binding.computeButton.setOnClickListener {
try {
forceReportWhistleBlowerStart()
} catch (e: Exception) {
e.message?.let {
this.showSnackBar(it)
}
}
}
this.paApplication?.reportWhistleBlower?.addListener(this) this.paApplication?.reportWhistleBlower?.addListener(this)
} }
@ -219,7 +199,7 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
private fun updateRows() { private fun updateRows() {
this.adapterRows.clear() this.adapterRows.clear()
if (this.reportSetups.isNotEmpty()) { if (this.reportSetups.size > 0) {
adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom)) adapterRows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.custom))
adapterRows.addAll(this.reportSetups) adapterRows.addAll(this.reportSetups)
} }
@ -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)
}
} }

@ -16,7 +16,6 @@ import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.review.ReviewException
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
@ -45,12 +44,7 @@ import net.pokeranalytics.android.ui.modules.datalist.DataListActivity
import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity import net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.SettingsRow import net.pokeranalytics.android.ui.view.rows.SettingsRow
import net.pokeranalytics.android.util.FileUtils import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.Language
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.StopNotificationManager
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.billing.PurchaseListener import net.pokeranalytics.android.util.billing.PurchaseListener
@ -59,7 +53,7 @@ import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.Date import java.util.*
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener { class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource, PurchaseListener {
@ -324,8 +318,6 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep
// completed // completed
} }
} else { } else {
val exception = (task.exception as ReviewException)
Timber.d("requestReviewFlow not successful = ${exception.message}")
// There was some problem, continue regardless of the result. // There was some problem, continue regardless of the result.
} }
} }

@ -3,8 +3,6 @@ package net.pokeranalytics.android.ui.fragment.components
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import net.pokeranalytics.android.PokerAnalyticsApplication import net.pokeranalytics.android.PokerAnalyticsApplication
@ -41,12 +39,6 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
v.setPadding(0, statusBarHeight, 0, 0)
insets
}
} }
override fun onResume() { override fun onResume() {

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

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

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

@ -5,8 +5,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -84,11 +82,6 @@ open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentab
initData() initData()
initUI() initUI()
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
report?.let { report?.let {
showResults() showResults()
} }

@ -117,8 +117,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
type = if (significant != null) { type = if (significant != null) {
val betAmount = significant.action.amount val betAmount = significant.action.amount
val remainingStack = computedAction.stackBeforeActing val remainingStack = computedAction.stackBeforeActing
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0 if (remainingStack != null && betAmount != null && remainingStack < betAmount) {
if (remainingStack != null && betAmount != null && (committedStack + remainingStack < betAmount)) {
Action.Type.CALL_ALLIN Action.Type.CALL_ALLIN
} else { } else {
Action.Type.RAISE_ALLIN Action.Type.RAISE_ALLIN
@ -131,10 +130,8 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
Action.Type.CALL -> { Action.Type.CALL -> {
getStreetLastSignificantAction(computedAction.street, index - 1)?.let { getStreetLastSignificantAction(computedAction.street, index - 1)?.let {
val betAmount = it.action.amount ?: 0.0 val betAmount = it.action.amount ?: 0.0
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0
val remainingStack = computedAction.stackBeforeActing val remainingStack = computedAction.stackBeforeActing
if (remainingStack != null && committedStack + remainingStack < betAmount) { if (remainingStack != null && remainingStack < betAmount) {
type = Action.Type.CALL_ALLIN type = Action.Type.CALL_ALLIN
} }
} ?: throw PAIllegalStateException("Can't call without a significant action") } ?: throw PAIllegalStateException("Can't call without a significant action")
@ -222,8 +219,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
} }
} }
Action.Type.BET, Action.Type.POT, Action.Type.RAISE -> { Action.Type.BET, Action.Type.POT, Action.Type.RAISE -> {
val committedStack = getPreviouslyCommittedAmount(index) ?: 0.0 if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
if (remainingStack != null && actionAmount != null && committedStack + remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL) setOf(Action.Type.FOLD, Action.Type.CALL)
} else { } else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN) setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.POT, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
@ -528,8 +524,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
created = true created = true
} }
val stack = val stack = this.filter { it.positionIndex == positionIndex }.sumByDouble { it.action.effectiveAmount }
this.filter { it.positionIndex == positionIndex }.sumOf { it.action.effectiveAmount }
playerSetup.stack = stack playerSetup.stack = stack
if (created) { if (created) {
@ -601,7 +596,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
*/ */
override fun getStreetNextCalls(index: Int): List<ComputedAction> { override fun getStreetNextCalls(index: Int): List<ComputedAction> {
val streetNextSignificantIndex = getStreetNextSignificantAction(index)?.action?.index val streetNextSignificantIndex = getStreetNextSignificantAction(index)?.action?.index
?: (this.lastIndexOfStreet(index) + 1) // +1 because of "until" ?: this.lastIndexOfStreet(index) + 1 // +1 because of "until"
return this.filter { return this.filter {
it.action.index in ((index + 1) until streetNextSignificantIndex) it.action.index in ((index + 1) until streetNextSignificantIndex)
&& (it.action.type?.isCall ?: false) && (it.action.type?.isCall ?: false)
@ -615,7 +610,7 @@ class ActionList(var listener: ActionListListener? = null) : ArrayList<ComputedA
} }
override fun totalPotSize(index: Int): Double { override fun totalPotSize(index: Int): Double {
return this.handHistory.anteSum + this.take(index).sumOf { it.action.effectiveAmount } return this.handHistory.anteSum + this.take(index).sumByDouble { it.action.effectiveAmount }
} }
/*** /***

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

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

@ -592,7 +592,6 @@ class ReplayerAnimator(var handHistory: HandHistory, var export: Boolean) {
this.drawer.drawTable(canvas, context) this.drawer.drawTable(canvas, context)
frameHandler(bitmap, vo) 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) super.onDraw(canvas)
this.animator.drawTable(canvas, context)
canvas?.let {
this.animator.drawTable(canvas, context)
}
} }
} }

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

@ -18,6 +18,7 @@ import com.github.mikephil.charting.data.*
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.tabs.TabLayout 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.R
import net.pokeranalytics.android.calculus.ComputedStat import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
@ -689,8 +690,7 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { override fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) {
if (row is CellResult) { if (row is CellResult) {
val timeUnit = itemView.findViewById<View>(R.id.timeUnit) itemView.timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background)
timeUnit.background = ContextCompat.getDrawable(itemView.context, row.background)
} }
} }

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

@ -7,7 +7,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.util.csv.DataType import net.pokeranalytics.android.util.csv.DataType
@ -50,24 +49,18 @@ class BackupOperator(var context: Context) {
private fun backupDataType(dataType: DataType) { private fun backupDataType(dataType: DataType) {
val data = Data.Builder() val data = Data.Builder()
.putInt(BackupWorker.ParamKeys.DATA.value, dataType.ordinal) .putInt(BackupTask.ParamKeys.DATA.value, dataType.ordinal)
var duration = 10L val backupTask = OneTimeWorkRequestBuilder<BackupTask>()
var unit = TimeUnit.HOURS .setInitialDelay(10, TimeUnit.HOURS)
if (BuildConfig.DEBUG) { // .setInitialDelay(10, TimeUnit.SECONDS)
duration = 1L
unit = TimeUnit.SECONDS
}
val backupWorker = OneTimeWorkRequestBuilder<BackupWorker>()
.setInitialDelay(duration, unit)
.setInputData(data.build()) .setInputData(data.build())
.addTag(dataType.workId) .addTag(dataType.workId)
.build() .build()
Timber.d(">>> create backupTask") Timber.d(">>> create backupTask")
WorkManager.getInstance(context).enqueueUniqueWork(dataType.workId, ExistingWorkPolicy.REPLACE, backupWorker) WorkManager.getInstance(context).enqueueUniqueWork(dataType.workId, ExistingWorkPolicy.REPLACE, backupTask)
} }
} }

@ -17,7 +17,7 @@ import timber.log.Timber
import java.util.* import java.util.*
class BackupWorker(var context: Context, var params: WorkerParameters) : Worker(context, params) { class BackupTask(var context: Context, var params: WorkerParameters) : Worker(context, params) {
enum class ParamKeys(val value: String) { enum class ParamKeys(val value: String) {
DATA("title"), DATA("title"),
@ -31,29 +31,20 @@ class BackupWorker(var context: Context, var params: WorkerParameters) : Worker(
val dataType = DataType.values()[dataTypeInt] val dataType = DataType.values()[dataTypeInt]
Preferences.getBackupEmail(context)?.let { email -> Preferences.getBackupEmail(context)?.let { email ->
val task = BackupTask(dataType, email, context) when(dataType) {
task.start() DataType.SESSION -> {
backupSessions(email)
}
DataType.TRANSACTION -> {
backupTransactions(email)
}
}
} }
return Result.success() return Result.success()
} }
} private fun backupSessions(email: String) {
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") Timber.d(">>>> backup sessions")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -62,13 +53,12 @@ class BackupTask(val dataType: DataType, val email: String, val context: Context
val fileName = "sessions_${Date().dateTimeFileFormatted}.csv" val fileName = "sessions_${Date().dateTimeFileFormatted}.csv"
CoroutineScope(context = Dispatchers.IO).launch { CoroutineScope(context = Dispatchers.IO).launch {
val success = BackupApi.backupFile(context, email, fileName, csv) BackupApi.backupFile(context, email, fileName, csv)
Preferences.setSessionsBackupSuccess(success, context)
} }
realm.close() realm.close()
} }
private fun backupTransactions() { private fun backupTransactions(email: String) {
Timber.d(">>>> backup transactions") Timber.d(">>>> backup transactions")
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -77,8 +67,7 @@ class BackupTask(val dataType: DataType, val email: String, val context: Context
val fileName = "transactions_${Date().dateTimeFileFormatted}.csv" val fileName = "transactions_${Date().dateTimeFileFormatted}.csv"
CoroutineScope(context = Dispatchers.IO).launch { CoroutineScope(context = Dispatchers.IO).launch {
val success = BackupApi.backupFile(context, email, fileName, csv) BackupApi.backupFile(context, email, fileName, csv)
Preferences.setTransactionsBackupSuccess(success, context)
} }
realm.close() realm.close()
} }

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

@ -33,7 +33,7 @@ class Preferences {
PATCH_SESSION_SETS("patchSessionSet"), PATCH_SESSION_SETS("patchSessionSet"),
PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"), PATCH_TRANSACTION_TYPES_NAMES("patchTransactionTypesNames"),
// PATCH_BLINDS_FORMAT("patchBlindFormat"), // PATCH_BLINDS_FORMAT("patchBlindFormat"),
PATCH_COMPUTABLE_RESULTS("patchPositiveSessions_v2"), PATCH_COMPUTABLE_RESULTS("patchPositiveSessions"),
PATCH_ZERO_TABLE("patchZeroTable"), PATCH_ZERO_TABLE("patchZeroTable"),
SHOW_STOP_NOTIFICATIONS("showStopNotifications"), SHOW_STOP_NOTIFICATIONS("showStopNotifications"),
ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"), ADD_NEW_TRANSACTION_TYPES("addNewTransactionTypes_transfer"),
@ -51,9 +51,7 @@ class Preferences {
LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"), LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"),
PATCH_RATED_AMOUNT("patchRatedAmount[new field]"), PATCH_RATED_AMOUNT("patchRatedAmount[new field]"),
BACKUP_EMAIL("backupEmail"), BACKUP_EMAIL("backupEmail"),
LANGUAGE_CODE("languageCode"), LANGUAGE_CODE("languageCode")
SESSIONS_BACKUP_SUCCESS("sessionsBackupSuccess"),
TRANSACTIONS_BACKUP_SUCCESS("transactionsBackupSuccess")
} }
enum class FeedMessage { enum class FeedMessage {
@ -354,21 +352,6 @@ class Preferences {
return getString(Keys.LANGUAGE_CODE, context) 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)
}
} }
} }

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

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

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

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

@ -2,15 +2,14 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar" android:id="@+id/appBar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/PokerAnalyticsTheme.Toolbar.Session" android:theme="@style/PokerAnalyticsTheme.Toolbar.Session"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -18,7 +17,7 @@
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:title="@string/hand_history" /> app:title="@string/hand_history" />
@ -30,14 +29,16 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottomBar" app:layout_constraintBottom_toTopOf="@+id/bottomBar"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBar" /> app:layout_constraintTop_toBottomOf="@+id/appBar" />
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/bottomBar" android:id="@+id/bottomBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?attr/actionBarSize"
android:layout_alignParentBottom="true"
android:layout_gravity="center"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways" app:layout_scrollFlags="scroll|enterAlways"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -47,8 +48,8 @@
<LinearLayout <LinearLayout
android:id="@+id/controls" android:id="@+id/controls"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="50dp"
android:fitsSystemWindows="true" android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">

@ -66,18 +66,4 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/computeButton"
style="@style/PokerAnalyticsTheme.FloatingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_outline_restart"
android:tint="@color/black"
app:fabSize="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -1,19 +1,19 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.9.24' ext.kotlin_version = '1.7.21'
repositories { repositories {
google() google()
mavenCentral() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.2.2' classpath 'com.android.tools.build:gradle:8.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'io.realm:realm-gradle-plugin:10.19.0' classpath 'io.realm:realm-gradle-plugin:10.3.1'
// crashlytics // crashlytics
classpath 'com.google.gms:google-services:4.4.2' classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2'
// serialization // serialization
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"

@ -30,9 +30,6 @@ org.gradle.configureondemand=true
# Enable parallel builds # Enable parallel builds
org.gradle.parallel=true org.gradle.parallel=true
# Enable Build Cache
#android.enableBuildCache=true
# Enable simple gradle caching # Enable simple gradle caching
org.gradle.caching=true org.gradle.caching=true
android.defaults.buildfeatures.buildconfig=true android.defaults.buildfeatures.buildconfig=true

@ -1 +1,2 @@
include ':app' include ':app'
include ':shared'
Loading…
Cancel
Save