diff --git a/.github/workflows/instrumented_tests.yml b/.github/workflows/instrumented_tests.yml
index 8bb28b93..12a278c4 100644
--- a/.github/workflows/instrumented_tests.yml
+++ b/.github/workflows/instrumented_tests.yml
@@ -1,8 +1,6 @@
name: Android Instrumented Tests
on:
- push:
- branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d33521..00000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 832ac50d..b268ef36 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,14 +4,6 @@
-
-
-
-
-
-
-
-
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
deleted file mode 100644
index 91f95584..00000000
--- a/.idea/deviceManager.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 4f8fa0eb..7061a0d6 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -19,19 +19,15 @@
-
-
-
-
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 6c5519f9..2bc46641 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,9 +1,4 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 53365ce2..9befcfde 100644
--- a/README.md
+++ b/README.md
@@ -70,13 +70,14 @@ There are some optional permissions, but they aren't necessary to the core audio
playing stuff.
- :telephone_receiver: **Phone State** : To handle incomming calls during a recording.
-- :world_map: **Location** : Some mediacodec like `acc` and `three_gpp` can add a additional
+- :world_map: **Location** : Some mediacodec like `acc` and `three_gpp` can add a
location data with the recording.You can view this location data on other devices which can read
metadata.
## :new: What's new
-The latest update to the **RecorderApp** contains some improvements with the media player
+The latest update to the **RecorderApp** makes the Player audio graph smoother alongside scrollable
+to seek player position
## :next_track_button: What's next
@@ -116,7 +117,7 @@ Contributions are always welcomed from the community
### :curly_loop: Feedback and Support
-A app is never perfect there may issue here and there which are not caught.If you encounter any
+AN app is never perfect there may issue here and there which are not caught.If you encounter any
issues, have suggestions for new features, or just want to share your thoughts, please don't
hesitate to reach out by creating a new [Issue](https://github.com/tuuhin/RecorderApp/issues) on
GitHub. Your feedback is invaluable!
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bc010d34..c436b8b5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -17,8 +17,8 @@ android {
applicationId = "com.eva.recorderapp"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.compileSdk.get().toInt()
- versionCode = 13
- versionName = "1.4.3"
+ versionCode = 14
+ versionName = "1.4.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/core/ui/src/main/java/com/eva/ui/theme/Color.kt b/core/ui/src/main/java/com/eva/ui/theme/Color.kt
index fd5dd531..b0698f6c 100644
--- a/core/ui/src/main/java/com/eva/ui/theme/Color.kt
+++ b/core/ui/src/main/java/com/eva/ui/theme/Color.kt
@@ -2,218 +2,91 @@ package com.eva.ui.theme
import androidx.compose.ui.graphics.Color
-val primaryLight = Color(0xFF006A60)
-val onPrimaryLight = Color(0xFFFFFFFF)
-val primaryContainerLight = Color(0xFF9EF2E4)
-val onPrimaryContainerLight = Color(0xFF005048)
-val secondaryLight = Color(0xFF00696E)
-val onSecondaryLight = Color(0xFFFFFFFF)
-val secondaryContainerLight = Color(0xFF9CF0F6)
-val onSecondaryContainerLight = Color(0xFF004F53)
-val tertiaryLight = Color(0xFF65558F)
-val onTertiaryLight = Color(0xFFFFFFFF)
-val tertiaryContainerLight = Color(0xFFE9DDFF)
-val onTertiaryContainerLight = Color(0xFF4D3D75)
-val errorLight = Color(0xFF904A43)
-val onErrorLight = Color(0xFFFFFFFF)
-val errorContainerLight = Color(0xFFFFDAD5)
-val onErrorContainerLight = Color(0xFF73342D)
-val backgroundLight = Color(0xFFF4FBF8)
-val onBackgroundLight = Color(0xFF161D1C)
-val surfaceLight = Color(0xFFF4FBFA)
-val onSurfaceLight = Color(0xFF161D1C)
-val surfaceVariantLight = Color(0xFFDAE5E3)
-val onSurfaceVariantLight = Color(0xFF3F4947)
-val outlineLight = Color(0xFF6F7978)
-val outlineVariantLight = Color(0xFFBEC9C7)
-val scrimLight = Color(0xFF000000)
-val inverseSurfaceLight = Color(0xFF2B3231)
-val inverseOnSurfaceLight = Color(0xFFECF2F1)
-val inversePrimaryLight = Color(0xFF82D5C8)
-val surfaceDimLight = Color(0xFFD5DBDA)
-val surfaceBrightLight = Color(0xFFF4FBFA)
-val surfaceContainerLowestLight = Color(0xFFFFFFFF)
-val surfaceContainerLowLight = Color(0xFFEFF5F4)
-val surfaceContainerLight = Color(0xFFE9EFEE)
-val surfaceContainerHighLight = Color(0xFFE3E9E8)
-val surfaceContainerHighestLight = Color(0xFFDDE4E3)
+val Seed = Color(0xFF006A60)
-val primaryLightMediumContrast = Color(0xFF003E37)
-val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
-val primaryContainerLightMediumContrast = Color(0xFF1D7A6F)
-val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
-val secondaryLightMediumContrast = Color(0xFF003D40)
-val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
-val secondaryContainerLightMediumContrast = Color(0xFF16797E)
-val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
-val tertiaryLightMediumContrast = Color(0xFF3C2C63)
-val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
-val tertiaryContainerLightMediumContrast = Color(0xFF74649F)
-val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
-val errorLightMediumContrast = Color(0xFF5E231E)
-val onErrorLightMediumContrast = Color(0xFFFFFFFF)
-val errorContainerLightMediumContrast = Color(0xFFA25850)
-val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
-val backgroundLightMediumContrast = Color(0xFFF4FBF8)
-val onBackgroundLightMediumContrast = Color(0xFF161D1C)
-val surfaceLightMediumContrast = Color(0xFFF4FBFA)
-val onSurfaceLightMediumContrast = Color(0xFF0C1212)
-val surfaceVariantLightMediumContrast = Color(0xFFDAE5E3)
-val onSurfaceVariantLightMediumContrast = Color(0xFF2E3837)
-val outlineLightMediumContrast = Color(0xFF4A5453)
-val outlineVariantLightMediumContrast = Color(0xFF656F6E)
-val scrimLightMediumContrast = Color(0xFF000000)
-val inverseSurfaceLightMediumContrast = Color(0xFF2B3231)
-val inverseOnSurfaceLightMediumContrast = Color(0xFFECF2F1)
-val inversePrimaryLightMediumContrast = Color(0xFF82D5C8)
-val surfaceDimLightMediumContrast = Color(0xFFC1C8C7)
-val surfaceBrightLightMediumContrast = Color(0xFFF4FBFA)
-val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
-val surfaceContainerLowLightMediumContrast = Color(0xFFEFF5F4)
-val surfaceContainerLightMediumContrast = Color(0xFFE3E9E8)
-val surfaceContainerHighLightMediumContrast = Color(0xFFD8DEDD)
-val surfaceContainerHighestLightMediumContrast = Color(0xFFCCD3D2)
+internal val PrimaryLight = Color(0xFF00675D)
+internal val OnPrimaryLight = Color(0xFFC0FFF4)
+internal val PrimaryContainerLight = Color(0xFF9AECDF)
+internal val OnPrimaryContainerLight = Color(0xFF005950)
+internal val InversePrimaryLight = Color(0xFF9FF2E4)
+internal val SecondaryLight = Color(0xFF00666B)
+internal val OnSecondaryLight = Color(0xFFCAFCFF)
+internal val SecondaryContainerLight = Color(0xFFA1F0F5)
+internal val OnSecondaryContainerLight = Color(0xFF005B60)
+internal val TertiaryLight = Color(0xFF62528C)
+internal val OnTertiaryLight = Color(0xFFF7F0FF)
+internal val TertiaryContainerLight = Color(0xFFC7B4F6)
+internal val OnTertiaryContainerLight = Color(0xFF403168)
+internal val BackgroundLight = Color(0xFFF1F8F7)
+internal val OnBackgroundLight = Color(0xFF293030)
+internal val SurfaceLight = Color(0xFFF1F8F7)
+internal val OnSurfaceLight = Color(0xFF293030)
+internal val SurfaceVariantLight = Color(0xFFD4DFDE)
+internal val OnSurfaceVariantLight = Color(0xFF565D5C)
+internal val SurfaceTintLight = Color(0xFF00675D)
+internal val InverseSurfaceLight = Color(0xFF090F0F)
+internal val InverseOnSurfaceLight = Color(0xFF979E9E)
+internal val ErrorLight = Color(0xFF9E3A32)
+internal val OnErrorLight = Color(0xFFFFEFED)
+internal val ErrorContainerLight = Color(0xFFFD8175)
+internal val OnErrorContainerLight = Color(0xFF731A16)
+internal val OutlineLight = Color(0xFF717878)
+internal val OutlineVariantLight = Color(0xFFA4AFAE)
+internal val ScrimLight = Color(0xFF000000)
+internal val SurfaceBrightLight = Color(0xFFF1F8F7)
+internal val SurfaceContainerLight = Color(0xFFE1EAE9)
+internal val SurfaceContainerHighLight = Color(0xFFDBE4E3)
+internal val SurfaceContainerHighestLight = Color(0xFFD4DFDE)
+internal val SurfaceContainerLowLight = Color(0xFFEAF2F1)
+internal val SurfaceContainerLowestLight = Color(0xFFFFFFFF)
+internal val SurfaceDimLight = Color(0xFFCBD7D6)
-val primaryLightHighContrast = Color(0xFF00332D)
-val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
-val primaryContainerLightHighContrast = Color(0xFF00534B)
-val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
-val secondaryLightHighContrast = Color(0xFF003234)
-val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
-val secondaryContainerLightHighContrast = Color(0xFF005256)
-val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
-val tertiaryLightHighContrast = Color(0xFF322258)
-val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
-val tertiaryContainerLightHighContrast = Color(0xFF4F4078)
-val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
-val errorLightHighContrast = Color(0xFF511A15)
-val onErrorLightHighContrast = Color(0xFFFFFFFF)
-val errorContainerLightHighContrast = Color(0xFF76362F)
-val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
-val backgroundLightHighContrast = Color(0xFFF4FBF8)
-val onBackgroundLightHighContrast = Color(0xFF161D1C)
-val surfaceLightHighContrast = Color(0xFFF4FBFA)
-val onSurfaceLightHighContrast = Color(0xFF000000)
-val surfaceVariantLightHighContrast = Color(0xFFDAE5E3)
-val onSurfaceVariantLightHighContrast = Color(0xFF000000)
-val outlineLightHighContrast = Color(0xFF242E2D)
-val outlineVariantLightHighContrast = Color(0xFF414B4A)
-val scrimLightHighContrast = Color(0xFF000000)
-val inverseSurfaceLightHighContrast = Color(0xFF2B3231)
-val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
-val inversePrimaryLightHighContrast = Color(0xFF82D5C8)
-val surfaceDimLightHighContrast = Color(0xFFB4BAB9)
-val surfaceBrightLightHighContrast = Color(0xFFF4FBFA)
-val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
-val surfaceContainerLowLightHighContrast = Color(0xFFECF2F1)
-val surfaceContainerLightHighContrast = Color(0xFFDDE4E3)
-val surfaceContainerHighLightHighContrast = Color(0xFFCFD6D5)
-val surfaceContainerHighestLightHighContrast = Color(0xFFC1C8C7)
+internal val PrimaryDark = Color(0xFF9AECDF)
+internal val OnPrimaryDark = Color(0xFF005950)
+internal val PrimaryContainerDark = Color(0xFF5DAEA3)
+internal val OnPrimaryContainerDark = Color(0xFF002824)
+internal val InversePrimaryDark = Color(0xFF036B61)
+internal val SecondaryDark = Color(0xFFA1F0F5)
+internal val OnSecondaryDark = Color(0xFF005B60)
+internal val SecondaryContainerDark = Color(0xFF00696E)
+internal val OnSecondaryContainerDark = Color(0xFFE0FDFF)
+internal val TertiaryDark = Color(0xFFD5C3FF)
+internal val OnTertiaryDark = Color(0xFF4A3A72)
+internal val TertiaryContainerDark = Color(0xFFC7B4F6)
+internal val OnTertiaryContainerDark = Color(0xFF403168)
+internal val BackgroundDark = Color(0xFF090F0F)
+internal val OnBackgroundDark = Color(0xFFE8EFEE)
+internal val SurfaceDark = Color(0xFF090F0F)
+internal val OnSurfaceDark = Color(0xFFE8EFEE)
+internal val SurfaceVariantDark = Color(0xFF1E2827)
+internal val OnSurfaceVariantDark = Color(0xFFA5ACAC)
+internal val SurfaceTintDark = Color(0xFF9AECDF)
+internal val InverseSurfaceDark = Color(0xFFF4FBFA)
+internal val InverseOnSurfaceDark = Color(0xFF4F5656)
+internal val ErrorDark = Color(0xFFF37A6E)
+internal val OnErrorDark = Color(0xFF4A0003)
+internal val ErrorContainerDark = Color(0xFF822620)
+internal val OnErrorContainerDark = Color(0xFFFF998F)
+internal val OutlineDark = Color(0xFF707776)
+internal val OutlineVariantDark = Color(0xFF404A49)
+internal val ScrimDark = Color(0xFF000000)
+internal val SurfaceBrightDark = Color(0xFF232E2E)
+internal val SurfaceContainerDark = Color(0xFF131B1B)
+internal val SurfaceContainerHighDark = Color(0xFF182121)
+internal val SurfaceContainerHighestDark = Color(0xFF1E2827)
+internal val SurfaceContainerLowDark = Color(0xFF0D1515)
+internal val SurfaceContainerLowestDark = Color(0xFF000000)
+internal val SurfaceDimDark = Color(0xFF090F0F)
-val primaryDark = Color(0xFF82D5C8)
-val onPrimaryDark = Color(0xFF003732)
-val primaryContainerDark = Color(0xFF005048)
-val onPrimaryContainerDark = Color(0xFF9EF2E4)
-val secondaryDark = Color(0xFF80D4D9)
-val onSecondaryDark = Color(0xFF003739)
-val secondaryContainerDark = Color(0xFF004F53)
-val onSecondaryContainerDark = Color(0xFF9CF0F6)
-val tertiaryDark = Color(0xFFCFBDFE)
-val onTertiaryDark = Color(0xFF36265D)
-val tertiaryContainerDark = Color(0xFF4D3D75)
-val onTertiaryContainerDark = Color(0xFFE9DDFF)
-val errorDark = Color(0xFFFFB4AB)
-val onErrorDark = Color(0xFF561E19)
-val errorContainerDark = Color(0xFF73342D)
-val onErrorContainerDark = Color(0xFFFFDAD5)
-val backgroundDark = Color(0xFF0E1513)
-val onBackgroundDark = Color(0xFFDDE4E1)
-val surfaceDark = Color(0xFF0E1514)
-val onSurfaceDark = Color(0xFFDDE4E3)
-val surfaceVariantDark = Color(0xFF3F4947)
-val onSurfaceVariantDark = Color(0xFFBEC9C7)
-val outlineDark = Color(0xFF889391)
-val outlineVariantDark = Color(0xFF3F4947)
-val scrimDark = Color(0xFF000000)
-val inverseSurfaceDark = Color(0xFFDDE4E3)
-val inverseOnSurfaceDark = Color(0xFF2B3231)
-val inversePrimaryDark = Color(0xFF006A60)
-val surfaceDimDark = Color(0xFF0E1514)
-val surfaceBrightDark = Color(0xFF343A3A)
-val surfaceContainerLowestDark = Color(0xFF090F0F)
-val surfaceContainerLowDark = Color(0xFF161D1C)
-val surfaceContainerDark = Color(0xFF1A2120)
-val surfaceContainerHighDark = Color(0xFF252B2B)
-val surfaceContainerHighestDark = Color(0xFF2F3636)
-
-val primaryDarkMediumContrast = Color(0xFF98ECDE)
-val onPrimaryDarkMediumContrast = Color(0xFF002B27)
-val primaryContainerDarkMediumContrast = Color(0xFF4A9E93)
-val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
-val secondaryDarkMediumContrast = Color(0xFF96EAEF)
-val onSecondaryDarkMediumContrast = Color(0xFF002B2D)
-val secondaryContainerDarkMediumContrast = Color(0xFF479DA2)
-val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
-val tertiaryDarkMediumContrast = Color(0xFFE3D6FF)
-val onTertiaryDarkMediumContrast = Color(0xFF2B1B52)
-val tertiaryContainerDarkMediumContrast = Color(0xFF9887C5)
-val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
-val errorDarkMediumContrast = Color(0xFFFFD2CC)
-val onErrorDarkMediumContrast = Color(0xFF48130F)
-val errorContainerDarkMediumContrast = Color(0xFFCC7B72)
-val onErrorContainerDarkMediumContrast = Color(0xFF000000)
-val backgroundDarkMediumContrast = Color(0xFF0E1513)
-val onBackgroundDarkMediumContrast = Color(0xFFDDE4E1)
-val surfaceDarkMediumContrast = Color(0xFF0E1514)
-val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
-val surfaceVariantDarkMediumContrast = Color(0xFF3F4947)
-val onSurfaceVariantDarkMediumContrast = Color(0xFFD4DEDC)
-val outlineDarkMediumContrast = Color(0xFFAAB4B2)
-val outlineVariantDarkMediumContrast = Color(0xFF889291)
-val scrimDarkMediumContrast = Color(0xFF000000)
-val inverseSurfaceDarkMediumContrast = Color(0xFFDDE4E3)
-val inverseOnSurfaceDarkMediumContrast = Color(0xFF252B2B)
-val inversePrimaryDarkMediumContrast = Color(0xFF00514A)
-val surfaceDimDarkMediumContrast = Color(0xFF0E1514)
-val surfaceBrightDarkMediumContrast = Color(0xFF3F4645)
-val surfaceContainerLowestDarkMediumContrast = Color(0xFF040808)
-val surfaceContainerLowDarkMediumContrast = Color(0xFF181F1E)
-val surfaceContainerDarkMediumContrast = Color(0xFF232929)
-val surfaceContainerHighDarkMediumContrast = Color(0xFF2D3433)
-val surfaceContainerHighestDarkMediumContrast = Color(0xFF383F3E)
-
-val primaryDarkHighContrast = Color(0xFFAFFFF2)
-val onPrimaryDarkHighContrast = Color(0xFF000000)
-val primaryContainerDarkHighContrast = Color(0xFF7ED1C5)
-val onPrimaryContainerDarkHighContrast = Color(0xFF000E0C)
-val secondaryDarkHighContrast = Color(0xFFBCFBFF)
-val onSecondaryDarkHighContrast = Color(0xFF000000)
-val secondaryContainerDarkHighContrast = Color(0xFF7CD0D5)
-val onSecondaryContainerDarkHighContrast = Color(0xFF000E0F)
-val tertiaryDarkHighContrast = Color(0xFFF5EDFF)
-val onTertiaryDarkHighContrast = Color(0xFF000000)
-val tertiaryContainerDarkHighContrast = Color(0xFFCBB9FA)
-val onTertiaryContainerDarkHighContrast = Color(0xFF0F0033)
-val errorDarkHighContrast = Color(0xFFFFECE9)
-val onErrorDarkHighContrast = Color(0xFF000000)
-val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
-val onErrorContainerDarkHighContrast = Color(0xFF220000)
-val backgroundDarkHighContrast = Color(0xFF0E1513)
-val onBackgroundDarkHighContrast = Color(0xFFDDE4E1)
-val surfaceDarkHighContrast = Color(0xFF0E1514)
-val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
-val surfaceVariantDarkHighContrast = Color(0xFF3F4947)
-val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
-val outlineDarkHighContrast = Color(0xFFE8F2F0)
-val outlineVariantDarkHighContrast = Color(0xFFBAC5C3)
-val scrimDarkHighContrast = Color(0xFF000000)
-val inverseSurfaceDarkHighContrast = Color(0xFFDDE4E3)
-val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
-val inversePrimaryDarkHighContrast = Color(0xFF00514A)
-val surfaceDimDarkHighContrast = Color(0xFF0E1514)
-val surfaceBrightDarkHighContrast = Color(0xFF4B5151)
-val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
-val surfaceContainerLowDarkHighContrast = Color(0xFF1A2120)
-val surfaceContainerDarkHighContrast = Color(0xFF2B3231)
-val surfaceContainerHighDarkHighContrast = Color(0xFF363D3C)
-val surfaceContainerHighestDarkHighContrast = Color(0xFF414848)
+internal val PrimaryFixed = Color(0xFF9AECDF)
+internal val PrimaryFixedDim = Color(0xFF8CDED1)
+internal val OnPrimaryFixed = Color(0xFF00443D)
+internal val OnPrimaryFixedVariant = Color(0xFF00635A)
+internal val SecondaryFixed = Color(0xFFA1F0F5)
+internal val SecondaryFixedDim = Color(0xFF93E1E7)
+internal val OnSecondaryFixed = Color(0xFF00474B)
+internal val OnSecondaryFixedVariant = Color(0xFF00666B)
+internal val TertiaryFixed = Color(0xFFC7B4F6)
+internal val TertiaryFixedDim = Color(0xFFB9A7E7)
+internal val OnTertiaryFixed = Color(0xFF2B1B52)
+internal val OnTertiaryFixedVariant = Color(0xFF493A72)
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/eva/ui/theme/Theme.kt b/core/ui/src/main/java/com/eva/ui/theme/Theme.kt
index 6baed005..9a9b8213 100644
--- a/core/ui/src/main/java/com/eva/ui/theme/Theme.kt
+++ b/core/ui/src/main/java/com/eva/ui/theme/Theme.kt
@@ -12,231 +12,105 @@ import androidx.compose.ui.platform.LocalContext
val lightScheme = lightColorScheme(
- primary = primaryLight,
- onPrimary = onPrimaryLight,
- primaryContainer = primaryContainerLight,
- onPrimaryContainer = onPrimaryContainerLight,
- secondary = secondaryLight,
- onSecondary = onSecondaryLight,
- secondaryContainer = secondaryContainerLight,
- onSecondaryContainer = onSecondaryContainerLight,
- tertiary = tertiaryLight,
- onTertiary = onTertiaryLight,
- tertiaryContainer = tertiaryContainerLight,
- onTertiaryContainer = onTertiaryContainerLight,
- error = errorLight,
- onError = onErrorLight,
- errorContainer = errorContainerLight,
- onErrorContainer = onErrorContainerLight,
- background = backgroundLight,
- onBackground = onBackgroundLight,
- surface = surfaceLight,
- onSurface = onSurfaceLight,
- surfaceVariant = surfaceVariantLight,
- onSurfaceVariant = onSurfaceVariantLight,
- outline = outlineLight,
- outlineVariant = outlineVariantLight,
- scrim = scrimLight,
- inverseSurface = inverseSurfaceLight,
- inverseOnSurface = inverseOnSurfaceLight,
- inversePrimary = inversePrimaryLight,
- surfaceDim = surfaceDimLight,
- surfaceBright = surfaceBrightLight,
- surfaceContainerLowest = surfaceContainerLowestLight,
- surfaceContainerLow = surfaceContainerLowLight,
- surfaceContainer = surfaceContainerLight,
- surfaceContainerHigh = surfaceContainerHighLight,
- surfaceContainerHighest = surfaceContainerHighestLight,
+ primary = PrimaryLight,
+ onPrimary = OnPrimaryLight,
+ primaryContainer = PrimaryContainerLight,
+ onPrimaryContainer = OnPrimaryContainerLight,
+ inversePrimary = InversePrimaryLight,
+ secondary = SecondaryLight,
+ onSecondary = OnSecondaryLight,
+ secondaryContainer = SecondaryContainerLight,
+ onSecondaryContainer = OnSecondaryContainerLight,
+ tertiary = TertiaryLight,
+ onTertiary = OnTertiaryLight,
+ tertiaryContainer = TertiaryContainerLight,
+ onTertiaryContainer = OnTertiaryContainerLight,
+ background = BackgroundLight,
+ onBackground = OnBackgroundLight,
+ surface = SurfaceLight,
+ onSurface = OnSurfaceLight,
+ surfaceVariant = SurfaceVariantLight,
+ onSurfaceVariant = OnSurfaceVariantLight,
+ surfaceTint = SurfaceTintLight,
+ inverseSurface = InverseSurfaceLight,
+ inverseOnSurface = InverseOnSurfaceLight,
+ error = ErrorLight,
+ onError = OnErrorLight,
+ errorContainer = ErrorContainerLight,
+ onErrorContainer = OnErrorContainerLight,
+ outline = OutlineLight,
+ outlineVariant = OutlineVariantLight,
+ scrim = ScrimLight,
+ surfaceBright = SurfaceBrightLight,
+ surfaceContainer = SurfaceContainerLight,
+ surfaceContainerHigh = SurfaceContainerHighLight,
+ surfaceContainerHighest = SurfaceContainerHighestLight,
+ surfaceContainerLow = SurfaceContainerLowLight,
+ surfaceContainerLowest = SurfaceContainerLowestLight,
+ surfaceDim = SurfaceDimLight,
+ primaryFixed = PrimaryFixed,
+ primaryFixedDim = PrimaryFixedDim,
+ onPrimaryFixed = OnPrimaryFixed,
+ onPrimaryFixedVariant = OnPrimaryFixedVariant,
+ secondaryFixed = SecondaryFixed,
+ secondaryFixedDim = SecondaryFixedDim,
+ onSecondaryFixed = OnSecondaryFixed,
+ onSecondaryFixedVariant = OnSecondaryFixedVariant,
+ tertiaryFixed = TertiaryFixed,
+ tertiaryFixedDim = TertiaryFixedDim,
+ onTertiaryFixed = OnTertiaryFixed,
+ onTertiaryFixedVariant = OnTertiaryFixedVariant,
)
val darkScheme = darkColorScheme(
- primary = primaryDark,
- onPrimary = onPrimaryDark,
- primaryContainer = primaryContainerDark,
- onPrimaryContainer = onPrimaryContainerDark,
- secondary = secondaryDark,
- onSecondary = onSecondaryDark,
- secondaryContainer = secondaryContainerDark,
- onSecondaryContainer = onSecondaryContainerDark,
- tertiary = tertiaryDark,
- onTertiary = onTertiaryDark,
- tertiaryContainer = tertiaryContainerDark,
- onTertiaryContainer = onTertiaryContainerDark,
- error = errorDark,
- onError = onErrorDark,
- errorContainer = errorContainerDark,
- onErrorContainer = onErrorContainerDark,
- background = backgroundDark,
- onBackground = onBackgroundDark,
- surface = surfaceDark,
- onSurface = onSurfaceDark,
- surfaceVariant = surfaceVariantDark,
- onSurfaceVariant = onSurfaceVariantDark,
- outline = outlineDark,
- outlineVariant = outlineVariantDark,
- scrim = scrimDark,
- inverseSurface = inverseSurfaceDark,
- inverseOnSurface = inverseOnSurfaceDark,
- inversePrimary = inversePrimaryDark,
- surfaceDim = surfaceDimDark,
- surfaceBright = surfaceBrightDark,
- surfaceContainerLowest = surfaceContainerLowestDark,
- surfaceContainerLow = surfaceContainerLowDark,
- surfaceContainer = surfaceContainerDark,
- surfaceContainerHigh = surfaceContainerHighDark,
- surfaceContainerHighest = surfaceContainerHighestDark,
-)
-
-private val mediumContrastLightColorScheme = lightColorScheme(
- primary = primaryLightMediumContrast,
- onPrimary = onPrimaryLightMediumContrast,
- primaryContainer = primaryContainerLightMediumContrast,
- onPrimaryContainer = onPrimaryContainerLightMediumContrast,
- secondary = secondaryLightMediumContrast,
- onSecondary = onSecondaryLightMediumContrast,
- secondaryContainer = secondaryContainerLightMediumContrast,
- onSecondaryContainer = onSecondaryContainerLightMediumContrast,
- tertiary = tertiaryLightMediumContrast,
- onTertiary = onTertiaryLightMediumContrast,
- tertiaryContainer = tertiaryContainerLightMediumContrast,
- onTertiaryContainer = onTertiaryContainerLightMediumContrast,
- error = errorLightMediumContrast,
- onError = onErrorLightMediumContrast,
- errorContainer = errorContainerLightMediumContrast,
- onErrorContainer = onErrorContainerLightMediumContrast,
- background = backgroundLightMediumContrast,
- onBackground = onBackgroundLightMediumContrast,
- surface = surfaceLightMediumContrast,
- onSurface = onSurfaceLightMediumContrast,
- surfaceVariant = surfaceVariantLightMediumContrast,
- onSurfaceVariant = onSurfaceVariantLightMediumContrast,
- outline = outlineLightMediumContrast,
- outlineVariant = outlineVariantLightMediumContrast,
- scrim = scrimLightMediumContrast,
- inverseSurface = inverseSurfaceLightMediumContrast,
- inverseOnSurface = inverseOnSurfaceLightMediumContrast,
- inversePrimary = inversePrimaryLightMediumContrast,
- surfaceDim = surfaceDimLightMediumContrast,
- surfaceBright = surfaceBrightLightMediumContrast,
- surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
- surfaceContainerLow = surfaceContainerLowLightMediumContrast,
- surfaceContainer = surfaceContainerLightMediumContrast,
- surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
- surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
-)
-
-private val highContrastLightColorScheme = lightColorScheme(
- primary = primaryLightHighContrast,
- onPrimary = onPrimaryLightHighContrast,
- primaryContainer = primaryContainerLightHighContrast,
- onPrimaryContainer = onPrimaryContainerLightHighContrast,
- secondary = secondaryLightHighContrast,
- onSecondary = onSecondaryLightHighContrast,
- secondaryContainer = secondaryContainerLightHighContrast,
- onSecondaryContainer = onSecondaryContainerLightHighContrast,
- tertiary = tertiaryLightHighContrast,
- onTertiary = onTertiaryLightHighContrast,
- tertiaryContainer = tertiaryContainerLightHighContrast,
- onTertiaryContainer = onTertiaryContainerLightHighContrast,
- error = errorLightHighContrast,
- onError = onErrorLightHighContrast,
- errorContainer = errorContainerLightHighContrast,
- onErrorContainer = onErrorContainerLightHighContrast,
- background = backgroundLightHighContrast,
- onBackground = onBackgroundLightHighContrast,
- surface = surfaceLightHighContrast,
- onSurface = onSurfaceLightHighContrast,
- surfaceVariant = surfaceVariantLightHighContrast,
- onSurfaceVariant = onSurfaceVariantLightHighContrast,
- outline = outlineLightHighContrast,
- outlineVariant = outlineVariantLightHighContrast,
- scrim = scrimLightHighContrast,
- inverseSurface = inverseSurfaceLightHighContrast,
- inverseOnSurface = inverseOnSurfaceLightHighContrast,
- inversePrimary = inversePrimaryLightHighContrast,
- surfaceDim = surfaceDimLightHighContrast,
- surfaceBright = surfaceBrightLightHighContrast,
- surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
- surfaceContainerLow = surfaceContainerLowLightHighContrast,
- surfaceContainer = surfaceContainerLightHighContrast,
- surfaceContainerHigh = surfaceContainerHighLightHighContrast,
- surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
-)
-
-private val mediumContrastDarkColorScheme = darkColorScheme(
- primary = primaryDarkMediumContrast,
- onPrimary = onPrimaryDarkMediumContrast,
- primaryContainer = primaryContainerDarkMediumContrast,
- onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
- secondary = secondaryDarkMediumContrast,
- onSecondary = onSecondaryDarkMediumContrast,
- secondaryContainer = secondaryContainerDarkMediumContrast,
- onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
- tertiary = tertiaryDarkMediumContrast,
- onTertiary = onTertiaryDarkMediumContrast,
- tertiaryContainer = tertiaryContainerDarkMediumContrast,
- onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
- error = errorDarkMediumContrast,
- onError = onErrorDarkMediumContrast,
- errorContainer = errorContainerDarkMediumContrast,
- onErrorContainer = onErrorContainerDarkMediumContrast,
- background = backgroundDarkMediumContrast,
- onBackground = onBackgroundDarkMediumContrast,
- surface = surfaceDarkMediumContrast,
- onSurface = onSurfaceDarkMediumContrast,
- surfaceVariant = surfaceVariantDarkMediumContrast,
- onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
- outline = outlineDarkMediumContrast,
- outlineVariant = outlineVariantDarkMediumContrast,
- scrim = scrimDarkMediumContrast,
- inverseSurface = inverseSurfaceDarkMediumContrast,
- inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
- inversePrimary = inversePrimaryDarkMediumContrast,
- surfaceDim = surfaceDimDarkMediumContrast,
- surfaceBright = surfaceBrightDarkMediumContrast,
- surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
- surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
- surfaceContainer = surfaceContainerDarkMediumContrast,
- surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
- surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
-)
-
-private val highContrastDarkColorScheme = darkColorScheme(
- primary = primaryDarkHighContrast,
- onPrimary = onPrimaryDarkHighContrast,
- primaryContainer = primaryContainerDarkHighContrast,
- onPrimaryContainer = onPrimaryContainerDarkHighContrast,
- secondary = secondaryDarkHighContrast,
- onSecondary = onSecondaryDarkHighContrast,
- secondaryContainer = secondaryContainerDarkHighContrast,
- onSecondaryContainer = onSecondaryContainerDarkHighContrast,
- tertiary = tertiaryDarkHighContrast,
- onTertiary = onTertiaryDarkHighContrast,
- tertiaryContainer = tertiaryContainerDarkHighContrast,
- onTertiaryContainer = onTertiaryContainerDarkHighContrast,
- error = errorDarkHighContrast,
- onError = onErrorDarkHighContrast,
- errorContainer = errorContainerDarkHighContrast,
- onErrorContainer = onErrorContainerDarkHighContrast,
- background = backgroundDarkHighContrast,
- onBackground = onBackgroundDarkHighContrast,
- surface = surfaceDarkHighContrast,
- onSurface = onSurfaceDarkHighContrast,
- surfaceVariant = surfaceVariantDarkHighContrast,
- onSurfaceVariant = onSurfaceVariantDarkHighContrast,
- outline = outlineDarkHighContrast,
- outlineVariant = outlineVariantDarkHighContrast,
- scrim = scrimDarkHighContrast,
- inverseSurface = inverseSurfaceDarkHighContrast,
- inverseOnSurface = inverseOnSurfaceDarkHighContrast,
- inversePrimary = inversePrimaryDarkHighContrast,
- surfaceDim = surfaceDimDarkHighContrast,
- surfaceBright = surfaceBrightDarkHighContrast,
- surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
- surfaceContainerLow = surfaceContainerLowDarkHighContrast,
- surfaceContainer = surfaceContainerDarkHighContrast,
- surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
- surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
+ primary = PrimaryDark,
+ onPrimary = OnPrimaryDark,
+ primaryContainer = PrimaryContainerDark,
+ onPrimaryContainer = OnPrimaryContainerDark,
+ inversePrimary = InversePrimaryDark,
+ secondary = SecondaryDark,
+ onSecondary = OnSecondaryDark,
+ secondaryContainer = SecondaryContainerDark,
+ onSecondaryContainer = OnSecondaryContainerDark,
+ tertiary = TertiaryDark,
+ onTertiary = OnTertiaryDark,
+ tertiaryContainer = TertiaryContainerDark,
+ onTertiaryContainer = OnTertiaryContainerDark,
+ background = BackgroundDark,
+ onBackground = OnBackgroundDark,
+ surface = SurfaceDark,
+ onSurface = OnSurfaceDark,
+ surfaceVariant = SurfaceVariantDark,
+ onSurfaceVariant = OnSurfaceVariantDark,
+ surfaceTint = SurfaceTintDark,
+ inverseSurface = InverseSurfaceDark,
+ inverseOnSurface = InverseOnSurfaceDark,
+ error = ErrorDark,
+ onError = OnErrorDark,
+ errorContainer = ErrorContainerDark,
+ onErrorContainer = OnErrorContainerDark,
+ outline = OutlineDark,
+ outlineVariant = OutlineVariantDark,
+ scrim = ScrimDark,
+ surfaceBright = SurfaceBrightDark,
+ surfaceContainer = SurfaceContainerDark,
+ surfaceContainerHigh = SurfaceContainerHighDark,
+ surfaceContainerHighest = SurfaceContainerHighestDark,
+ surfaceContainerLow = SurfaceContainerLowDark,
+ surfaceContainerLowest = SurfaceContainerLowestDark,
+ surfaceDim = SurfaceDimDark,
+ primaryFixed = PrimaryFixed,
+ primaryFixedDim = PrimaryFixedDim,
+ onPrimaryFixed = OnPrimaryFixed,
+ onPrimaryFixedVariant = OnPrimaryFixedVariant,
+ secondaryFixed = SecondaryFixed,
+ secondaryFixedDim = SecondaryFixedDim,
+ onSecondaryFixed = OnSecondaryFixed,
+ onSecondaryFixedVariant = OnSecondaryFixedVariant,
+ tertiaryFixed = TertiaryFixed,
+ tertiaryFixedDim = TertiaryFixedDim,
+ onTertiaryFixed = OnTertiaryFixed,
+ onTertiaryFixedVariant = OnTertiaryFixedVariant,
)
@Composable
diff --git a/core/ui/src/main/res/values-bn/strings.xml b/core/ui/src/main/res/values-bn/strings.xml
index faada8bf..2ca5cee6 100644
--- a/core/ui/src/main/res/values-bn/strings.xml
+++ b/core/ui/src/main/res/values-bn/strings.xml
@@ -236,4 +236,7 @@
শুরু করুন
%s এ স্বাগত
লিস্টে ফিরে যান
+ বাতিল
+ এডিট বাতিল করুন
+ এডিটর স্ক্রিন থেকে বেরিয়ে গেলে আপনার সম্পাদনা বাতিল হয়ে যাবে
\ No newline at end of file
diff --git a/core/ui/src/main/res/values-hi/strings.xml b/core/ui/src/main/res/values-hi/strings.xml
index 35daefc7..74faafb5 100644
--- a/core/ui/src/main/res/values-hi/strings.xml
+++ b/core/ui/src/main/res/values-hi/strings.xml
@@ -236,4 +236,7 @@
जारी रखें
%s में आपका स्वागत है
लिस्ट पर वापस जाएँ
+ बाहर निकलें
+ एडिट रद्द करें
+ एडिटर स्क्रीन से बाहर निकलने पर आपकी चल रही एडिटिंग हट जाएगी
\ No newline at end of file
diff --git a/core/ui/src/main/res/values-night/colors.xml b/core/ui/src/main/res/values-night/colors.xml
index 14f90802..04212206 100644
--- a/core/ui/src/main/res/values-night/colors.xml
+++ b/core/ui/src/main/res/values-night/colors.xml
@@ -2,12 +2,12 @@
@color/white
- #14140C
- #E7E2D5
- #82D5C8
- #003732
- #005048
- #9EF2E4
+ #090F0F
+ #E8EFEE
+ #9AECDF
+ #005950
+ #5DAEA3
+ #002824
#FFFFFF
#FFA9D799
diff --git a/core/ui/src/main/res/values/colors.xml b/core/ui/src/main/res/values/colors.xml
index f3615d38..f1ba2e26 100644
--- a/core/ui/src/main/res/values/colors.xml
+++ b/core/ui/src/main/res/values/colors.xml
@@ -5,12 +5,12 @@
#B40000
#DCDCDC
- #F4FBFA
- #161D1C
- #006A60
- #FFFFFF
- #9EF2E4
- #005048
+ #F1F8F7
+ #293030
+ #00675D
+ #C0FFF4
+ #9AECDF
+ #005950
#000000
#FFF1FFE8
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index d58ee03e..7c31ebd4 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -269,4 +269,7 @@
Continue
Welcome to %s
Go back to List
+ Exit
+ Cancel Edit
+ Exiting the editor screen will cancel your ongoing editing
\ No newline at end of file
diff --git a/data/editor/src/main/java/com/eva/editor/data/transformer/AudioFileToComposition.kt b/data/editor/src/main/java/com/eva/editor/data/transformer/AudioFileToComposition.kt
index 04187fa8..dbd91f9f 100644
--- a/data/editor/src/main/java/com/eva/editor/data/transformer/AudioFileToComposition.kt
+++ b/data/editor/src/main/java/com/eva/editor/data/transformer/AudioFileToComposition.kt
@@ -2,6 +2,7 @@ package com.eva.editor.data.transformer
import android.util.Log
import androidx.annotation.OptIn
+import androidx.media3.common.C
import androidx.media3.common.Effect
import androidx.media3.common.MediaItem
import androidx.media3.common.audio.AudioProcessor
@@ -62,13 +63,16 @@ internal fun AudioFileModel.toComposition(
Log.d(TAG, "CLIPPING :${ranges.joinToString("|")}")
- val itemSequence = EditedMediaItemSequence.Builder().also { builder ->
- editableItems.forEachIndexed { idx, item ->
- builder.addItem(item)
- if (gap > Duration.ZERO && idx + 1 < editableItems.size)
- builder.addGap(gap.inWholeMicroseconds)
+ val itemSequence = EditedMediaItemSequence
+ .Builder(setOf(C.TRACK_TYPE_AUDIO))
+ .also { builder ->
+ editableItems.forEachIndexed { idx, item ->
+ builder.addItem(item)
+ if (gap > Duration.ZERO && idx + 1 < editableItems.size)
+ builder.addGap(gap.inWholeMicroseconds)
+ }
}
- }.build()
+ .build()
return Composition.Builder(itemSequence)
.build()
diff --git a/data/editor/src/main/java/com/eva/editor/data/transformer/TransformerAwaitResult.kt b/data/editor/src/main/java/com/eva/editor/data/transformer/TransformerAwaitResult.kt
index fc3809b3..35db582e 100644
--- a/data/editor/src/main/java/com/eva/editor/data/transformer/TransformerAwaitResult.kt
+++ b/data/editor/src/main/java/com/eva/editor/data/transformer/TransformerAwaitResult.kt
@@ -83,7 +83,7 @@ internal suspend fun Transformer.awaitResults(
@UnstableApi
suspend fun Transformer.awaitResults(mediaItem: MediaItem, outputUri: String): String {
val editMediaItem = EditedMediaItem.Builder(mediaItem).build()
- val sequence = EditedMediaItemSequence.Builder(editMediaItem).build()
+ val sequence = EditedMediaItemSequence.withAudioFrom(listOf(editMediaItem))
val composition = Composition.Builder(sequence).build()
return awaitResults(composition, outputUri)
}
\ No newline at end of file
diff --git a/data/editor/src/main/java/com/eva/editor/domain/model/AudioClipConfig.kt b/data/editor/src/main/java/com/eva/editor/domain/model/AudioClipConfig.kt
index cecfc8c0..e1644954 100644
--- a/data/editor/src/main/java/com/eva/editor/domain/model/AudioClipConfig.kt
+++ b/data/editor/src/main/java/com/eva/editor/domain/model/AudioClipConfig.kt
@@ -1,12 +1,19 @@
package com.eva.editor.domain.model
import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
data class AudioClipConfig(
val start: Duration = 0.seconds,
val end: Duration = 1.seconds,
) {
+
+ constructor(
+ startMillis: Int,
+ endMillis: Int
+ ) : this(startMillis.milliseconds, endMillis.milliseconds)
+
fun validate(totalDuration: Duration): Boolean {
return hasMinimumDuration && start.isPositive() && end <= totalDuration
}
diff --git a/data/player/build.gradle.kts b/data/player/build.gradle.kts
index f1cac244..c761e30b 100644
--- a/data/player/build.gradle.kts
+++ b/data/player/build.gradle.kts
@@ -12,6 +12,7 @@ dependencies {
implementation(libs.androidx.media3.common)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.session)
+ implementation(libs.androidx.media3.inspector)
// futures to coroutine
implementation(libs.androidx.concurrent.futures.ktx)
diff --git a/data/player/src/main/java/com/eva/player/data/AudioMetadataRetrieverImpl.kt b/data/player/src/main/java/com/eva/player/data/AudioMetadataRetrieverImpl.kt
index 6a70dde3..5ddd5800 100644
--- a/data/player/src/main/java/com/eva/player/data/AudioMetadataRetrieverImpl.kt
+++ b/data/player/src/main/java/com/eva/player/data/AudioMetadataRetrieverImpl.kt
@@ -4,8 +4,8 @@ import android.content.Context
import androidx.concurrent.futures.await
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.MetadataRetriever
import androidx.media3.exoplayer.source.MediaSource
+import androidx.media3.inspector.MetadataRetriever
import com.eva.player.domain.AudioMetadataRetriever
import com.eva.recordings.domain.models.AudioFileModel
import kotlin.time.Duration
diff --git a/data/player/src/main/java/com/eva/player/data/player/AudioFilePlayerImpl.kt b/data/player/src/main/java/com/eva/player/data/player/AudioFilePlayerImpl.kt
index b4f81501..1335318e 100644
--- a/data/player/src/main/java/com/eva/player/data/player/AudioFilePlayerImpl.kt
+++ b/data/player/src/main/java/com/eva/player/data/player/AudioFilePlayerImpl.kt
@@ -1,7 +1,9 @@
package com.eva.player.data.player
import android.util.Log
+import androidx.annotation.OptIn
import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
import com.eva.player.data.util.computeIsPlayerPlaying
import com.eva.player.data.util.computePlayerTrackData
import com.eva.player.data.util.toMediaItem
@@ -55,14 +57,14 @@ internal class AudioFilePlayerImpl(private val player: Player) : AudioFilePlayer
player.repeatMode = repeatMode
}
+ @OptIn(UnstableApi::class)
override fun onMuteDevice() {
val command = player.isCommandAvailable(Player.COMMAND_SET_VOLUME)
if (!command) {
Log.w(LOGGER, "PLAYER COMMAND NOT FOUND")
return
}
- val isStreamMuted = player.volume == 0f
- player.volume = if (isStreamMuted) 1f else 0f
+ if (player.volume == .0f) player.unmute() else player.mute()
}
override suspend fun preparePlayer(audio: AudioFileModel): Result {
diff --git a/data/recordings/src/main/java/com/eva/recordings/data/provider/AudioInfoExtractorImpl.kt b/data/recordings/src/main/java/com/eva/recordings/data/provider/AudioInfoExtractorImpl.kt
new file mode 100644
index 00000000..89816e7c
--- /dev/null
+++ b/data/recordings/src/main/java/com/eva/recordings/data/provider/AudioInfoExtractorImpl.kt
@@ -0,0 +1,67 @@
+package com.eva.recordings.data.provider
+
+import android.content.Context
+import android.media.MediaExtractor
+import android.media.MediaFormat
+import android.media.MediaMetadataRetriever
+import android.os.Build
+import androidx.core.net.toUri
+import com.eva.datastore.domain.repository.RecorderAudioSettingsRepo
+import com.eva.location.domain.repository.LocationAddressProvider
+import com.eva.location.domain.utils.parseLocationFromString
+import com.eva.recordings.domain.models.MediaMetaDataInfo
+import com.eva.recordings.domain.provider.AudioInfoExtractor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+
+class AudioInfoExtractorImpl(
+ private val context: Context,
+ private val settings: RecorderAudioSettingsRepo,
+ private val addressProvider: LocationAddressProvider,
+) : AudioInfoExtractor {
+
+ override suspend fun extractMediaData(uri: String): MediaMetaDataInfo? {
+ val extractor = MediaExtractor()
+ val retriever = MediaMetadataRetriever()
+ try {
+ val audioFileUri = uri.toUri()
+ return withContext(Dispatchers.IO) {// set source
+ extractor.setDataSource(context, audioFileUri, null)
+ retriever.setDataSource(context, audioFileUri)
+ // its accountable that there is a single track
+ val mediaFormat = extractor.getTrackFormat(0)
+ val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
+
+ val sampleRate = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)
+ ?.toIntOrNull() ?: 0
+ } else mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
+
+ val locationAsString = async {
+ val audioSettings = settings.audioSettings()
+ if (!audioSettings.addLocationInfoInRecording) return@async null
+ val locationString =
+ retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
+ parseLocationFromString(locationString)
+ ?.let { addressProvider.invoke(it).getOrNull() }
+ }
+ val bitRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
+ ?.toIntOrNull() ?: mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE)
+
+ MediaMetaDataInfo(
+ channelCount = channelCount,
+ sampleRate = sampleRate,
+ bitRate = bitRate,
+ locationString = locationAsString.await()
+ )
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ } finally {
+ retriever.release()
+ extractor.release()
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/recordings/src/main/java/com/eva/recordings/data/provider/PlayerFileProviderImpl.kt b/data/recordings/src/main/java/com/eva/recordings/data/provider/PlayerFileProviderImpl.kt
index 9a31a697..de7bb240 100644
--- a/data/recordings/src/main/java/com/eva/recordings/data/provider/PlayerFileProviderImpl.kt
+++ b/data/recordings/src/main/java/com/eva/recordings/data/provider/PlayerFileProviderImpl.kt
@@ -5,31 +5,21 @@ import android.content.ContentUris
import android.content.Context
import android.database.ContentObserver
import android.database.Cursor
-import android.media.MediaExtractor
-import android.media.MediaFormat
-import android.media.MediaMetadataRetriever
-import android.net.Uri
-import android.os.Build
import android.provider.MediaStore
import android.util.Log
-import androidx.core.net.toUri
import androidx.core.os.bundleOf
-import com.eva.datastore.domain.repository.RecorderAudioSettingsRepo
-import com.eva.location.domain.repository.LocationAddressProvider
-import com.eva.location.domain.utils.parseLocationFromString
import com.eva.recordings.BuildConfig
import com.eva.recordings.data.utils.evaluateWithTimeRead
import com.eva.recordings.data.wrapper.RecordingsConstants
import com.eva.recordings.data.wrapper.RecordingsContentResolverWrapper
import com.eva.recordings.domain.exceptions.InvalidAudioFileIdException
import com.eva.recordings.domain.models.AudioFileModel
-import com.eva.recordings.domain.models.MediaMetaDataInfo
+import com.eva.recordings.domain.provider.AudioInfoExtractor
import com.eva.recordings.domain.provider.PlayerFileProvider
import com.eva.recordings.domain.provider.ResourcedDetailedRecordingModel
import com.eva.utils.Resource
import com.eva.utils.toLocalDateTime
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@@ -41,9 +31,8 @@ import kotlin.time.Duration.Companion.seconds
private const val TAG = "PLAYER_FILE_PROVIDER"
internal class PlayerFileProviderImpl(
- private val context: Context,
- private val settings: RecorderAudioSettingsRepo,
- private val addressProvider: LocationAddressProvider,
+ context: Context,
+ private val extractor: AudioInfoExtractor,
) : RecordingsContentResolverWrapper(context), PlayerFileProvider {
private val _projection: Array
@@ -94,7 +83,7 @@ internal class PlayerFileProviderImpl(
readTime = BuildConfig.DEBUG
) {
if (!readMetaData) return@evaluateWithTimeRead null
- extractMediaInfo(model.fileUri.toUri())
+ extractor.extractMediaData(model.fileUri)
}
val modelWithMetaData = model.copy(metaData = metaData)
// send data with metadata
@@ -159,7 +148,7 @@ internal class PlayerFileProviderImpl(
readTime = BuildConfig.DEBUG
) {
if (!readMetaData) return@use result
- extractMediaInfo(result.fileUri.toUri())
+ extractor.extractMediaData(result.fileUri)
}
result.copy(metaData = metaData)
} ?: return@withContext Result.failure(InvalidAudioFileIdException())
@@ -168,50 +157,6 @@ internal class PlayerFileProviderImpl(
}
- private suspend fun extractMediaInfo(uri: Uri): MediaMetaDataInfo? {
- val extractor = MediaExtractor()
- val retriever = MediaMetadataRetriever()
- try {
- return withContext(Dispatchers.IO) {// set source
- extractor.setDataSource(context, uri, null)
- retriever.setDataSource(context, uri)
- // its accountable that there is a single track
- val mediaFormat = extractor.getTrackFormat(0)
- val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
-
- val sampleRate = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)
- ?.toIntOrNull() ?: 0
- } else mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
-
- val locationAsString = async {
- val audioSettings = settings.audioSettings()
- if (!audioSettings.addLocationInfoInRecording) return@async null
- val locationString =
- retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
- parseLocationFromString(locationString)
- ?.let { addressProvider.invoke(it).getOrNull() }
- }
-
- val bitRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
- ?.toIntOrNull() ?: 0
-
- MediaMetaDataInfo(
- channelCount = channelCount,
- sampleRate = sampleRate,
- bitRate = bitRate,
- locationString = locationAsString.await()
- )
- }
- } catch (e: Exception) {
- e.printStackTrace()
- return null
- } finally {
- retriever.release()
- extractor.release()
- }
- }
-
private suspend fun evaluateValuesFromCursor(cursor: Cursor): AudioFileModel? {
return withContext(Dispatchers.IO) {
diff --git a/data/recordings/src/main/java/com/eva/recordings/di/RecordingsProviderModule.kt b/data/recordings/src/main/java/com/eva/recordings/di/RecordingsProviderModule.kt
index f246fbbe..a9eead0a 100644
--- a/data/recordings/src/main/java/com/eva/recordings/di/RecordingsProviderModule.kt
+++ b/data/recordings/src/main/java/com/eva/recordings/di/RecordingsProviderModule.kt
@@ -8,6 +8,7 @@ import com.eva.datastore.domain.repository.RecorderAudioSettingsRepo
import com.eva.datastore.domain.repository.RecorderFileSettingsRepo
import com.eva.location.domain.repository.LocationAddressProvider
import com.eva.recordings.data.RecordingWidgetInteractorImpl
+import com.eva.recordings.data.provider.AudioInfoExtractorImpl
import com.eva.recordings.data.provider.PlayerFileProviderImpl
import com.eva.recordings.data.provider.RecorderFileProviderImpl
import com.eva.recordings.data.provider.RecordingSecondaryDataProviderImpl
@@ -15,6 +16,7 @@ import com.eva.recordings.data.provider.StorageInfoProviderImpl
import com.eva.recordings.data.provider.TrashRecordingsProviderApi29Impl
import com.eva.recordings.data.provider.TrashRecordingsProviderImpl
import com.eva.recordings.data.provider.VoiceRecordingsProviderImpl
+import com.eva.recordings.domain.provider.AudioInfoExtractor
import com.eva.recordings.domain.provider.PlayerFileProvider
import com.eva.recordings.domain.provider.RecorderFileProvider
import com.eva.recordings.domain.provider.RecordingsSecondaryDataProvider
@@ -33,6 +35,15 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object RecordingsProviderModule {
+ @Provides
+ @Singleton
+ fun providesAudioExtractor(
+ @ApplicationContext context: Context,
+ locationProvider: LocationAddressProvider,
+ settings: RecorderAudioSettingsRepo,
+ ): AudioInfoExtractor =
+ AudioInfoExtractorImpl(context, settings, locationProvider)
+
@Provides
@Singleton
fun providesRecordingsProvider(
@@ -66,9 +77,8 @@ object RecordingsProviderModule {
@Singleton
fun providesPlayerFileProvider(
@ApplicationContext context: Context,
- locationProvider: LocationAddressProvider,
- settings: RecorderAudioSettingsRepo,
- ): PlayerFileProvider = PlayerFileProviderImpl(context, settings, locationProvider)
+ extractor: AudioInfoExtractor,
+ ): PlayerFileProvider = PlayerFileProviderImpl(context, extractor)
@Provides
diff --git a/data/recordings/src/main/java/com/eva/recordings/domain/provider/AudioInfoExtractor.kt b/data/recordings/src/main/java/com/eva/recordings/domain/provider/AudioInfoExtractor.kt
new file mode 100644
index 00000000..ba8cef6d
--- /dev/null
+++ b/data/recordings/src/main/java/com/eva/recordings/domain/provider/AudioInfoExtractor.kt
@@ -0,0 +1,8 @@
+package com.eva.recordings.domain.provider
+
+import com.eva.recordings.domain.models.MediaMetaDataInfo
+
+fun interface AudioInfoExtractor {
+
+ suspend fun extractMediaData(uri: String): MediaMetaDataInfo?
+}
\ No newline at end of file
diff --git a/data/visualizer/build.gradle.kts b/data/visualizer/build.gradle.kts
index 991d3701..4be8ce50 100644
--- a/data/visualizer/build.gradle.kts
+++ b/data/visualizer/build.gradle.kts
@@ -8,5 +8,6 @@ android {
}
dependencies {
+ implementation(libs.androidx.media3.inspector)
implementation(project(":data:recordings"))
}
\ No newline at end of file
diff --git a/data/visualizer/src/main/java/com/com/visualizer/data/AudioVisualizerImpl.kt b/data/visualizer/src/main/java/com/com/visualizer/data/AudioVisualizerImpl.kt
index 58152ed6..86b6524d 100644
--- a/data/visualizer/src/main/java/com/com/visualizer/data/AudioVisualizerImpl.kt
+++ b/data/visualizer/src/main/java/com/com/visualizer/data/AudioVisualizerImpl.kt
@@ -2,8 +2,14 @@ package com.com.visualizer.data
import android.content.Context
import android.util.Log
+import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.lifecycle.LifecycleOwner
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DataSource
+import androidx.media3.datasource.DefaultDataSource
+import androidx.media3.extractor.DefaultExtractorsFactory
+import androidx.media3.extractor.ExtractorsFactory
import com.com.visualizer.domain.AudioVisualizer
import com.com.visualizer.domain.ThreadController
import com.com.visualizer.domain.VisualizerState
@@ -22,11 +28,19 @@ import kotlinx.coroutines.sync.withLock
private const val TAG = "PLAIN_VISUALIZER"
+@OptIn(UnstableApi::class)
internal class AudioVisualizerImpl(
- private val context: Context,
+ private val extractor: ExtractorsFactory,
+ private val dataSource: DataSource.Factory,
private val threadHandler: ThreadController
) : AudioVisualizer {
+ constructor(context: Context, threadHandler: ThreadController) : this(
+ extractor = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true),
+ dataSource = DefaultDataSource.Factory(context),
+ threadHandler = threadHandler
+ )
+
private var _decoder: MediaCodecPCMDataDecoder? = null
private val _lock = Mutex()
@@ -81,7 +95,7 @@ internal class AudioVisualizerImpl(
// decoder work is done so we can kill it now
if (handler != null) threadHandler.stopThread(handler)
}
- decoder.initiateExtraction(context, fileUri.toUri())
+ decoder.initiateExtraction(extractor, dataSource, fileUri.toUri())
} catch (e: Exception) {
Log.e(TAG, "CANNOT DECODE THIS URI", e)
Result.failure(e)
diff --git a/data/visualizer/src/main/java/com/com/visualizer/data/MediaCodecPCMDataDecoder.kt b/data/visualizer/src/main/java/com/com/visualizer/data/MediaCodecPCMDataDecoder.kt
index 7ea51f91..d437eb50 100644
--- a/data/visualizer/src/main/java/com/com/visualizer/data/MediaCodecPCMDataDecoder.kt
+++ b/data/visualizer/src/main/java/com/com/visualizer/data/MediaCodecPCMDataDecoder.kt
@@ -1,16 +1,18 @@
package com.com.visualizer.data
-import android.content.Context
import android.media.MediaCodec
import android.media.MediaCodecList
-import android.media.MediaExtractor
import android.media.MediaFormat
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
-import com.com.visualizer.domain.exception.ExtractorNoTrackFoundException
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DataSource
+import androidx.media3.extractor.ExtractorsFactory
+import androidx.media3.inspector.MediaExtractorCompat
import com.com.visualizer.domain.exception.InvalidMimeTypeException
+import com.com.visualizer.domain.exception.MediaExtractorException
import com.com.visualizer.utils.isThreadAlive
import com.com.visualizer.utils.safePOST
import kotlinx.coroutines.CancellationException
@@ -39,6 +41,7 @@ private const val CODEC_TAG = "CODEC_CALLBACK"
private const val PROCESSING_TAG = "CODEC_PROCESSING"
private const val EXTRACTOR_TAG = "MEDIA_EXTRACTOR"
+@UnstableApi
@OptIn(ExperimentalAtomicApi::class)
internal class MediaCodecPCMDataDecoder(
private val seekDurationMillis: Int,
@@ -51,7 +54,7 @@ internal class MediaCodecPCMDataDecoder(
private var _mediaCodec: MediaCodec? = null
@Volatile
- private var _extractor: MediaExtractor? = null
+ private var _extractor: MediaExtractorCompat? = null
@Volatile
private var _codecState = MediaCodecState.RELEASED
@@ -111,7 +114,10 @@ internal class MediaCodecPCMDataDecoder(
return
}
// seek the extractor as we don't need extra data
- extractor.seekTo(_currentTimeInMs.load() * 1_000L, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
+ extractor.seekTo(
+ _currentTimeInMs.load() * 1_000L,
+ MediaExtractorCompat.SEEK_TO_CLOSEST_SYNC
+ )
val sampleSize = extractor.readSampleData(inputBuffer, 0)
// sample size is zero thus processing done END_OF_STREAM
@@ -191,7 +197,10 @@ internal class MediaCodecPCMDataDecoder(
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
- Log.d(CODEC_TAG, "MEDIA FORMAT CHANGED: $format")
+ if (format.keys.contains(MediaFormat.KEY_DURATION)) {
+ _totalTimeInMs.store(format.duration?.inWholeMilliseconds ?: 0L)
+ }
+ Log.d(CODEC_TAG, "MEDIA FORMAT CHANGED: KEYS:${format.keys}")
}
private fun handleFloatArray(pcm: FloatArray) {
@@ -269,27 +278,38 @@ internal class MediaCodecPCMDataDecoder(
_onDecodeComplete = listener
}
- suspend fun initiateExtraction(context: Context, fileURI: Uri): Result =
- withContext(ioDispatcher) {
- _extractor?.release()
- _extractor = MediaExtractor().apply {
- setDataSource(context, fileURI, null)
- }
- val format = _extractor?.getTrackFormat(0)
- val mimeType = format?.mimeType
+ suspend fun initiateExtraction(
+ extractor: ExtractorsFactory,
+ dataSource: DataSource.Factory,
+ fileURI: Uri
+ ): Result = withContext(ioDispatcher) {
+ _extractor?.release()
+ _extractor = MediaExtractorCompat(extractor, dataSource).apply {
+ setDataSource(fileURI, 0)
+ }
+ // check track count
+ if (_extractor?.trackCount == 0)
+ return@withContext Result.failure(MediaExtractorException("No track found"))
- if (mimeType == null || !mimeType.startsWith("audio"))
- return@withContext Result.failure(InvalidMimeTypeException())
+ val format = _extractor!!.getTrackFormat(0)
+ val mimeType = format.mimeType
- if (_extractor?.trackCount == 0)
- return@withContext Result.failure(ExtractorNoTrackFoundException())
+ // check audio duration
+ if (!format.keys.contains(MediaFormat.KEY_DURATION))
+ return@withContext Result.failure(MediaExtractorException("Cannot determine track duration"))
- Log.i(EXTRACTOR_TAG, "EXTRACTOR PREPARED")
- _extractor?.selectTrack(0)
- _totalTimeInMs.store(format.duration.inWholeMilliseconds)
- initiateCodec(format)
- Result.success(Unit)
- }
+ // check mime type
+ if (mimeType == null || !mimeType.startsWith("audio"))
+ return@withContext Result.failure(InvalidMimeTypeException())
+
+ // set the total time
+ _totalTimeInMs.store(format.duration?.inWholeMilliseconds ?: 0L)
+
+ Log.i(EXTRACTOR_TAG, "EXTRACTOR PREPARED")
+ _extractor?.selectTrack(0)
+ initiateCodec(format)
+ Result.success(Unit)
+ }
private fun initiateCodec(format: MediaFormat) {
diff --git a/data/visualizer/src/main/java/com/com/visualizer/data/MediaFormatExt.kt b/data/visualizer/src/main/java/com/com/visualizer/data/MediaFormatExt.kt
index 804d243b..1da26dd2 100644
--- a/data/visualizer/src/main/java/com/com/visualizer/data/MediaFormatExt.kt
+++ b/data/visualizer/src/main/java/com/com/visualizer/data/MediaFormatExt.kt
@@ -24,11 +24,15 @@ internal val MediaFormat.channels: Int
internal val MediaFormat.sampleRate: Int
get() = getInteger(MediaFormat.KEY_SAMPLE_RATE)
-internal val MediaFormat.duration: Duration
- get() = getLong(MediaFormat.KEY_DURATION).microseconds
+internal val MediaFormat.duration: Duration?
+ get() = if (containsKey(MediaFormat.KEY_DURATION))
+ getLong(MediaFormat.KEY_DURATION).microseconds
+ else null
internal val MediaFormat.mimeType: String?
- get() = getString(MediaFormat.KEY_MIME)
+ get() = if (containsKey(MediaFormat.KEY_MIME))
+ getString(MediaFormat.KEY_MIME)
+ else null
internal val MediaCodec.BufferInfo.isEndOfStream: Boolean
get() = flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
diff --git a/data/visualizer/src/main/java/com/com/visualizer/domain/exception/ExtractorNoTrackFoundException.kt b/data/visualizer/src/main/java/com/com/visualizer/domain/exception/ExtractorNoTrackFoundException.kt
deleted file mode 100644
index f78f21d7..00000000
--- a/data/visualizer/src/main/java/com/com/visualizer/domain/exception/ExtractorNoTrackFoundException.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.com.visualizer.domain.exception
-
-class ExtractorNoTrackFoundException : Exception("No tracks found in the associated URI")
\ No newline at end of file
diff --git a/data/visualizer/src/main/java/com/com/visualizer/domain/exception/MediaExtractorException.kt b/data/visualizer/src/main/java/com/com/visualizer/domain/exception/MediaExtractorException.kt
new file mode 100644
index 00000000..a033bbe2
--- /dev/null
+++ b/data/visualizer/src/main/java/com/com/visualizer/domain/exception/MediaExtractorException.kt
@@ -0,0 +1,3 @@
+package com.com.visualizer.domain.exception
+
+class MediaExtractorException(override val message: String) : Exception(message)
\ No newline at end of file
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorRoute.kt b/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorRoute.kt
index 675c7955..771ca45d 100644
--- a/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorRoute.kt
+++ b/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorRoute.kt
@@ -86,6 +86,15 @@ fun NavGraphBuilder.audioEditorRoute(controller: NavController) =
}
}
+ LaunchedEffect(lifecycleOwner) {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ editorViewModel.clipConfigs.collectLatest {
+ // when clip configs are updated the compressed visuals also get updated
+ visualsViewmodel.updateClipConfigs(it)
+ }
+ }
+ }
+
CompositionLocalProvider(LocalSharedTransitionVisibilityScopeProvider provides this) {
AudioEditorScreen(
loadState = loadState,
@@ -98,6 +107,11 @@ fun NavGraphBuilder.audioEditorRoute(controller: NavController) =
isMediaEdited = isMediaEdited,
undoRedoState = undoRedoState,
transformationState = transformationState,
+ onDismissScreen = {
+ if (controller.previousBackStackEntry != null) {
+ controller.popBackStack()
+ }
+ },
navigation = {
if (controller.previousBackStackEntry != null) {
IconButton(onClick = dropUnlessResumed(block = controller::popBackStack)) {
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorScreen.kt b/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorScreen.kt
index 7a3d0826..5c9e8448 100644
--- a/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorScreen.kt
+++ b/feature/editor/src/main/java/com/eva/feature_editor/AudioEditorScreen.kt
@@ -1,5 +1,6 @@
package com.eva.feature_editor
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -24,6 +25,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -31,9 +33,11 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.DialogProperties
import com.eva.editor.domain.model.AudioClipConfig
import com.eva.feature_editor.composables.AudioClipChipRow
import com.eva.feature_editor.composables.EditorActionsAndControls
+import com.eva.feature_editor.composables.EditorBackHandlerDialog
import com.eva.feature_editor.composables.EditorTopBar
import com.eva.feature_editor.composables.PlayerTrimSelector
import com.eva.feature_editor.composables.TransformBottomSheet
@@ -76,10 +80,16 @@ internal fun AudioEditorScreen(
undoRedoState: UndoRedoState = UndoRedoState(),
transformationState: TransformationState = TransformationState(),
navigation: @Composable () -> Unit = {},
+ onDismissScreen: () -> Unit = {},
) {
val snackBarHostProvider = LocalSnackBarProvider.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+ val isBackHandlerEnabled by rememberSaveable(undoRedoState.holdHistory) {
+ mutableStateOf(undoRedoState.holdHistory)
+ }
+ var showDialog by remember { mutableStateOf(false) }
+
var showSheet by remember { mutableStateOf(false) }
val bottomSheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
@@ -97,6 +107,15 @@ internal fun AudioEditorScreen(
showSheet = showSheet
)
+ BackHandler(enabled = isBackHandlerEnabled, onBack = { showDialog = true })
+
+ EditorBackHandlerDialog(
+ showDialog = showDialog,
+ onConfirm = onDismissScreen,
+ onDismiss = { showDialog = false },
+ properties = DialogProperties(dismissOnClickOutside = false)
+ )
+
Scaffold(
topBar = {
EditorTopBar(
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/composables/EditorActionsAndControls.kt b/feature/editor/src/main/java/com/eva/feature_editor/composables/EditorActionsAndControls.kt
index 6b0454e7..5cc503cb 100644
--- a/feature/editor/src/main/java/com/eva/feature_editor/composables/EditorActionsAndControls.kt
+++ b/feature/editor/src/main/java/com/eva/feature_editor/composables/EditorActionsAndControls.kt
@@ -44,6 +44,7 @@ private fun EditorActionsAndControls(
onCutMedia: () -> Unit,
onPlay: () -> Unit,
onPause: () -> Unit,
+ onSeekEnd: () -> Unit = {},
playButtonColor: Color = MaterialTheme.colorScheme.primary,
actionButtonColor: Color = MaterialTheme.colorScheme.secondary,
) {
@@ -54,7 +55,8 @@ private fun EditorActionsAndControls(
) {
PlayerTrackSlider2(
trackData = trackData,
- onSeekComplete = onSeek
+ onSeek = onSeek,
+ onSeekEnd = onSeekEnd
)
//actions
Row(
@@ -158,6 +160,8 @@ internal fun EditorActionsAndControls(
onSeek = { onEvent(EditorScreenEvent.OnSeekTrack(it)) },
onPlay = { onEvent(EditorScreenEvent.PlayAudio) },
onPause = { onEvent(EditorScreenEvent.PauseAudio) },
+ onSeekEnd = { onEvent(EditorScreenEvent.OnSeekTrackEnd) },
onCropMedia = { onEvent(EditorScreenEvent.OnEditAction(AudioEditAction.CROP)) },
- onCutMedia = { onEvent(EditorScreenEvent.OnEditAction(AudioEditAction.CUT)) })
+ onCutMedia = { onEvent(EditorScreenEvent.OnEditAction(AudioEditAction.CUT)) },
+ )
}
\ No newline at end of file
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/composables/ExitEditorScreenConfirmDialog.kt b/feature/editor/src/main/java/com/eva/feature_editor/composables/ExitEditorScreenConfirmDialog.kt
new file mode 100644
index 00000000..7f0767ba
--- /dev/null
+++ b/feature/editor/src/main/java/com/eva/feature_editor/composables/ExitEditorScreenConfirmDialog.kt
@@ -0,0 +1,72 @@
+package com.eva.feature_editor.composables
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.window.DialogProperties
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.eva.ui.R
+import com.eva.ui.theme.RecorderAppTheme
+
+@Composable
+fun EditorBackHandlerDialog(
+ showDialog: Boolean,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+ properties: DialogProperties = DialogProperties()
+) {
+ val lifeCycleOwner = LocalLifecycleOwner.current
+ val lifecycleState by lifeCycleOwner.lifecycle.currentStateFlow.collectAsState()
+
+ if (!showDialog) return
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ Button(
+ onClick = onConfirm,
+ enabled = lifecycleState.isAtLeast(Lifecycle.State.RESUMED),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Text(
+ text = stringResource(R.string.action_exit),
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = onDismiss,
+ enabled = lifecycleState.isAtLeast(Lifecycle.State.RESUMED),
+ colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary)
+ ) {
+ Text(stringResource(R.string.action_cancel))
+ }
+ },
+ title = { Text(text = stringResource(R.string.editor_screen_exiting_warning_dialog_title)) },
+ text = { Text(text = stringResource(R.string.editor_screen_exiting_warning_dialog_desc)) },
+ modifier = modifier,
+ properties = properties,
+ )
+}
+
+@PreviewLightDark
+@Composable
+private fun EditorBackHandlerDialogPreview() = RecorderAppTheme {
+ EditorBackHandlerDialog(showDialog = true, onConfirm = {}, onDismiss = {})
+}
\ No newline at end of file
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/event/EditorScreenEvent.kt b/feature/editor/src/main/java/com/eva/feature_editor/event/EditorScreenEvent.kt
index 562cbdf7..b63e7a9d 100644
--- a/feature/editor/src/main/java/com/eva/feature_editor/event/EditorScreenEvent.kt
+++ b/feature/editor/src/main/java/com/eva/feature_editor/event/EditorScreenEvent.kt
@@ -10,7 +10,6 @@ sealed interface EditorScreenEvent {
data object PauseAudio : EditorScreenEvent
data class OnClipConfigChange(val config: AudioClipConfig) : EditorScreenEvent
- data class OnSeekTrack(val duration: Duration) : EditorScreenEvent
data class OnEditAction(val action: AudioEditAction) : EditorScreenEvent
@@ -21,4 +20,7 @@ sealed interface EditorScreenEvent {
data object OnCancelTransformation : EditorScreenEvent
data object OnDismissExportSheet : EditorScreenEvent
data object OnSaveExportFile : EditorScreenEvent
+
+ data class OnSeekTrack(val duration: Duration) : EditorScreenEvent
+ data object OnSeekTrackEnd : EditorScreenEvent
}
\ No newline at end of file
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/undoredo/UndoRedoState.kt b/feature/editor/src/main/java/com/eva/feature_editor/undoredo/UndoRedoState.kt
index ff86ccb1..cc979a42 100644
--- a/feature/editor/src/main/java/com/eva/feature_editor/undoredo/UndoRedoState.kt
+++ b/feature/editor/src/main/java/com/eva/feature_editor/undoredo/UndoRedoState.kt
@@ -1,3 +1,10 @@
package com.eva.feature_editor.undoredo
-data class UndoRedoState(val canUndo: Boolean = false, val canRedo: Boolean = false)
+data class UndoRedoState(
+ val canUndo: Boolean = false,
+ val canRedo: Boolean = false
+) {
+
+ val holdHistory: Boolean
+ get() = canUndo || canRedo
+}
diff --git a/feature/editor/src/main/java/com/eva/feature_editor/viewmodel/AudioEditorViewModel.kt b/feature/editor/src/main/java/com/eva/feature_editor/viewmodel/AudioEditorViewModel.kt
index 9c8da937..d48877c2 100644
--- a/feature/editor/src/main/java/com/eva/feature_editor/viewmodel/AudioEditorViewModel.kt
+++ b/feature/editor/src/main/java/com/eva/feature_editor/viewmodel/AudioEditorViewModel.kt
@@ -13,6 +13,7 @@ import com.eva.feature_editor.event.TransformationState
import com.eva.feature_editor.undoredo.UndoRedoManager
import com.eva.feature_editor.undoredo.UndoRedoState
import com.eva.player.domain.model.PlayerTrackData
+import com.eva.player_shared.state.PlayerTrackUIState
import com.eva.recordings.domain.models.AudioFileModel
import com.eva.recordings.domain.provider.PlayerFileProvider
import com.eva.ui.viewmodel.AppViewModel
@@ -49,6 +50,8 @@ internal class AudioEditorViewModel @AssistedInject constructor(
private val player: SimpleAudioPlayer,
) : AppViewModel() {
+ private val _trackUIState = PlayerTrackUIState()
+
private val _currentFile = MutableStateFlow(null)
private val _lastEditAction = MutableStateFlow(AudioEditAction.CROP)
private val _exportFileUri = MutableStateFlow(null)
@@ -98,11 +101,12 @@ internal class AudioEditorViewModel @AssistedInject constructor(
initialValue = false
)
- val trackData = player.trackInfoAsFlow.stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(1_000L),
- initialValue = PlayerTrackData()
- )
+ val trackData = _trackUIState.controllablePlayerTrackData(player.trackInfoAsFlow)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(1_000L),
+ initialValue = PlayerTrackData()
+ )
private val _uiEvents = MutableSharedFlow()
override val uiEvent: SharedFlow
@@ -113,7 +117,6 @@ internal class AudioEditorViewModel @AssistedInject constructor(
EditorScreenEvent.PauseAudio -> viewModelScope.launch { player.pausePlayer() }
EditorScreenEvent.PlayAudio -> viewModelScope.launch { player.startOrResumePlayer() }
is EditorScreenEvent.OnClipConfigChange -> updateClipConfig(event.config)
- is EditorScreenEvent.OnSeekTrack -> player.onSeekDuration(event.duration)
is EditorScreenEvent.OnEditAction -> validateAndApplyEditViaAction(event.action)
EditorScreenEvent.BeginTransformation -> finalExport()
EditorScreenEvent.OnDismissExportSheet -> onCancelExport()
@@ -121,6 +124,13 @@ internal class AudioEditorViewModel @AssistedInject constructor(
EditorScreenEvent.OnRedoEdit -> onUndoOrRedoConfigs(false)
EditorScreenEvent.OnUndoEdit -> onUndoOrRedoConfigs(true)
EditorScreenEvent.OnCancelTransformation -> cancelFinalExport()
+ is EditorScreenEvent.OnSeekTrack -> viewModelScope.launch {
+ _trackUIState.onSliderValueChange(event.duration)
+ }
+
+ EditorScreenEvent.OnSeekTrackEnd -> viewModelScope.launch {
+ _trackUIState.onInteractionFinished { duration -> player.onSeekDuration(duration) }
+ }
}
fun setPlayerItem() = viewModelScope.launch {
@@ -211,8 +221,9 @@ internal class AudioEditorViewModel @AssistedInject constructor(
val fileModel = _currentFile.value ?: return
val clipData = _clipData.updateAndGet { clipConfig } ?: return
// again if track data is not
- val trackData = this@AudioEditorViewModel.trackData.value.let { if (it.allPositiveAndFinite) it else null }
- ?: PlayerTrackData(Duration.ZERO, fileModel.duration)
+ val trackData =
+ this@AudioEditorViewModel.trackData.value.let { if (it.allPositiveAndFinite) it else null }
+ ?: PlayerTrackData(Duration.ZERO, fileModel.duration)
if (trackData.current in clipData.start..clipData.end) return
if (!clipData.hasMinimumDuration) {
diff --git a/feature/player-shared/build.gradle.kts b/feature/player-shared/build.gradle.kts
index ff9af596..0518aab6 100644
--- a/feature/player-shared/build.gradle.kts
+++ b/feature/player-shared/build.gradle.kts
@@ -27,4 +27,6 @@ dependencies {
implementation(project(":data:recordings"))
implementation(project(":data:interactions"))
implementation(project(":data:use_case"))
+
+ testImplementation(libs.mockk)
}
\ No newline at end of file
diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt b/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt
index 4bd8f361..3a959da4 100644
--- a/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt
+++ b/feature/player-shared/src/main/java/com/eva/player_shared/PlayerVisualizerViewmodel.kt
@@ -111,15 +111,20 @@ class PlayerVisualizerViewmodel @Inject constructor(
}
- private fun updatesOnConfigChange() {
- combine(visualizer.normalizedVisualization, _clipConfigs) { visuals, configs ->
+ private fun updatesOnConfigChange() = combine(
+ visualizer.normalizedVisualization,
+ _clipConfigs
+ ) { visuals, configs ->
+ try {
val newVisuals = visuals.updateArrayViaConfigs(
configs = configs,
timeInMillisPerBar = RecorderConstants.RECORDER_AMPLITUDES_BUFFER_SIZE
)
_compressedVisualization.update { newVisuals }
- }.launchIn(viewModelScope)
- }
+ } catch (_: IllegalStateException) {
+ _uiEvents.emit(UIEvents.ShowSnackBar("Cannot update visuals"))
+ }
+ }.launchIn(viewModelScope)
override fun onCleared() {
visualizer.cleanUp()
diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider.kt b/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider.kt
index 086d9b3e..8b623424 100644
--- a/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider.kt
+++ b/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider.kt
@@ -18,10 +18,18 @@ import kotlinx.coroutines.launch
import kotlin.math.round
import kotlin.time.Duration
+@Deprecated(
+ message = "This version of the player track slider is im compatible with track data info,the player state is being hoisted outside the composable scope use PlayerTrackSlider2",
+ replaceWith = ReplaceWith(
+ "PlayerTrackSlider2",
+ "com.eva.player_shared.composables.PlayerTrackSlider2"
+ ),
+ level = DeprecationLevel.ERROR,
+)
@Composable
fun PlayerTrackSlider(
trackData: PlayerTrackData,
- onSeekComplete: (Duration) -> Unit,
+ onSeek: (Duration) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
@@ -53,7 +61,7 @@ fun PlayerTrackSlider(
onValueChangeFinished = {
scope.launch {
controller.sliderCleanUp {
- onSeekComplete(seekAmountByUser)
+ onSeek(seekAmountByUser)
}
}
},
diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider2.kt b/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider2.kt
index 8e53b9e1..dede22ed 100644
--- a/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider2.kt
+++ b/feature/player-shared/src/main/java/com/eva/player_shared/composables/PlayerTrackSlider2.kt
@@ -1,5 +1,6 @@
package com.eva.player_shared.composables
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
@@ -10,16 +11,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.eva.player.domain.model.PlayerTrackData
-import com.eva.player_shared.state.PlayerSliderController
import com.eva.ui.theme.RecorderAppTheme
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.time.Duration
@@ -29,42 +26,31 @@ import kotlin.time.Duration.Companion.seconds
@Composable
fun PlayerTrackSlider2(
trackData: () -> PlayerTrackData,
- onSeekComplete: (Duration) -> Unit,
+ onSeek: (Duration) -> Unit,
modifier: Modifier = Modifier,
- enabled: Boolean = true
+ onSeekEnd: () -> Unit = {},
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
- // slider controller
- val controller = remember { PlayerSliderController() }
- val isUserControlled by controller.isSeekByUser.collectAsStateWithLifecycle(false)
- val seekAmountByUser by controller.seekAmountByUser.collectAsStateWithLifecycle()
-
- val currentOnSeekComplete by rememberUpdatedState(onSeekComplete)
-
- val state = remember { SliderState(value = trackData().playRatio) }
+ val state = remember {
+ SliderState(
+ value = trackData().playRatio,
+ valueRange = 0f..1f,
+ onValueChangeFinished = onSeekEnd,
+ ).also { state ->
+ // on value change will be called only when the value changed completed
+ // via interactions not update
+ state.onValueChange = { value ->
+ val seekAmount = trackData().calculateSeekAmount(value)
+ onSeek(seekAmount)
+ }
+ }
+ }
+ // update the track ratio when changed
LaunchedEffect(state) {
-
- // Basic state update updated by the player
snapshotFlow { trackData().playRatio }
- .filter { !state.isDragging }
- .onEach { state.value = it }
- .launchIn(this)
-
- // If the slider is being drag , updated by the user
- snapshotFlow { state.value }
- .filter { state.isDragging }
- .onEach { seek ->
- val playerSeekAmount = trackData().calculateSeekAmount(seek)
- controller.onSliderSlide(playerSeekAmount)
- }.launchIn(this)
-
- // now if it's not being dragged but user controlled then send seek completed
- snapshotFlow { !state.isDragging && isUserControlled }
- .filter { it }
- .onEach {
- controller.sliderCleanUp()
- currentOnSeekComplete(seekAmountByUser)
- }
+ .onEach { value -> state.value = value }
.launchIn(this)
}
@@ -76,7 +62,8 @@ fun PlayerTrackSlider2(
thumbColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.outlineVariant
),
- modifier = modifier
+ modifier = modifier,
+ interactionSource = interactionSource,
)
}
@@ -87,6 +74,6 @@ private fun PlayerTrackSlider2Preview() = RecorderAppTheme {
PlayerTrackSlider2(
trackData = { trackState },
- onSeekComplete = { trackState = trackState.copy(current = it) },
+ onSeek = { trackState = trackState.copy(current = it) },
)
}
\ No newline at end of file
diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/state/PlayerTrackUIState.kt b/feature/player-shared/src/main/java/com/eva/player_shared/state/PlayerTrackUIState.kt
new file mode 100644
index 00000000..9243f49e
--- /dev/null
+++ b/feature/player-shared/src/main/java/com/eva/player_shared/state/PlayerTrackUIState.kt
@@ -0,0 +1,63 @@
+package com.eva.player_shared.state
+
+import android.util.Log
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import com.eva.player.domain.model.PlayerTrackData
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+private const val TAG = "TrackUIState"
+
+@OptIn(
+ FlowPreview::class,
+ ExperimentalCoroutinesApi::class
+)
+class PlayerTrackUIState {
+
+ private val _mutex = MutatorMutex()
+
+ private val _seekAmountByUser = MutableStateFlow(Duration.ZERO)
+ private val _isSeekByUser = MutableStateFlow(false)
+
+ fun controllablePlayerTrackData(originalTrackData: Flow): Flow {
+ return _isSeekByUser
+ .debounce { isSeeking -> if (isSeeking) 0.milliseconds else 75.milliseconds }
+ .distinctUntilChanged()
+ .flatMapLatest { isSeeking ->
+ Log.d(TAG, "IS SEEKING $isSeeking")
+ // if the user is not seeking the item original track data is given
+ if (!isSeeking) return@flatMapLatest originalTrackData
+ // now user is seeking so we provide the seek data
+ combine(originalTrackData, _seekAmountByUser) { track, current ->
+ track.copy(current = current)
+ }
+ }
+ }
+
+ suspend fun onSliderValueChange(seekAmount: Duration) {
+ _mutex.mutate(MutatePriority.UserInput) {
+ Log.d(TAG, "SLIDER POSITION CHANGE")
+ _isSeekByUser.value = true
+ _seekAmountByUser.value = seekAmount
+ }
+ }
+
+ suspend fun onInteractionFinished(onSeekComplete: (Duration) -> Unit) {
+ if (!_isSeekByUser.value) return
+ Log.d(TAG, "SLIDER INTERACTION COMPLETED")
+ // Complete the seek operation
+ _mutex.mutate(MutatePriority.PreventUserInput) {
+ onSeekComplete(_seekAmountByUser.value)
+ _isSeekByUser.value = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/player-shared/src/main/java/com/eva/player_shared/util/AudioConfigsToVisuals.kt b/feature/player-shared/src/main/java/com/eva/player_shared/util/AudioConfigsToVisuals.kt
index 9f2568f8..40cf6818 100644
--- a/feature/player-shared/src/main/java/com/eva/player_shared/util/AudioConfigsToVisuals.kt
+++ b/feature/player-shared/src/main/java/com/eva/player_shared/util/AudioConfigsToVisuals.kt
@@ -5,6 +5,7 @@ import com.eva.editor.domain.AudioConfigToActionList
import com.eva.editor.domain.model.AudioEditAction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
import kotlin.math.max
import kotlin.math.min
@@ -12,47 +13,50 @@ private const val TAG = "PLAYER_CONFIG_SETTER"
internal suspend fun FloatArray.updateArrayViaConfigs(
configs: AudioConfigToActionList,
- timeInMillisPerBar: Int = 100
+ timeInMillisPerBar: Int = 100,
+ dispatcher: CoroutineContext = Dispatchers.Default
): FloatArray {
- return withContext(Dispatchers.Default) {
+ require(timeInMillisPerBar > 0) { "timeInMillisPerBar must be positive" }
+ return withContext(dispatcher) {
if (configs.isEmpty()) return@withContext this@updateArrayViaConfigs
var modifiedArray = copyOf()
-
Log.d(TAG, "INITIAL SIZE :${size}")
- // need to apply the config from back to front
- configs.reversed().forEachIndexed { iteration, (config, action) ->
- val startSample = (config.start.inWholeMilliseconds / timeInMillisPerBar).toInt()
- val endSample = (config.end.inWholeMilliseconds / timeInMillisPerBar).toInt()
-
- val validStart = max(0, min(startSample, modifiedArray.size))
- val validEnd = max(0, min(endSample, modifiedArray.size))
+ Log.d(TAG, "=========================================================")
+ configs.forEachIndexed { iteration, (config, action) ->
+ val startIndex = (config.start.inWholeMilliseconds / timeInMillisPerBar).toInt()
+ val endIndex = (config.end.inWholeMilliseconds / timeInMillisPerBar).toInt()
- if (validStart <= validEnd) {
+ val validStart = max(0, min(startIndex, modifiedArray.size))
+ val validEnd = max(0, min(endIndex, modifiedArray.size))
- val message = when (action) {
- AudioEditAction.CROP -> "NEW START :${validStart} NEW END:$validEnd"
- AudioEditAction.CUT -> "START1 :0 END1:$validStart || START2 :$validEnd END2: ${modifiedArray.size}"
- }
+ if (validStart >= validEnd) {
+ val error = "INVALID RANGE: [$validStart:$validEnd] ACT:$action I_TER:$iteration"
+ Log.e(TAG, error)
+ throw IllegalStateException(error)
+ }
- Log.i(TAG, "ITERATION:$iteration $message")
+ Log.d(TAG, "ITERATION:$iteration")
- modifiedArray = when (action) {
- AudioEditAction.CUT -> {
- val before = modifiedArray.copyOfRange(0, validStart)
- val after = modifiedArray.copyOfRange(validEnd, modifiedArray.size)
- before + after
- }
+ when (action) {
+ AudioEditAction.CUT -> {
+ Log.d(TAG, "ACTION:CUT [START :${validStart} END:$validEnd]")
+ val before = modifiedArray.copyOfRange(0, validStart)
+ val after = modifiedArray.copyOfRange(validEnd, modifiedArray.size)
+ modifiedArray = before + after
+ }
- AudioEditAction.CROP ->
- modifiedArray.copyOfRange(validStart, validEnd)
+ AudioEditAction.CROP -> {
+ Log.d(
+ TAG,
+ "ACTION:CROP [START :0 END:$validStart] ---xx-- [START:$validEnd END: ${modifiedArray.size}]"
+ )
+ modifiedArray = modifiedArray.copyOfRange(validStart, validEnd)
}
- } else {
- val error = "Invalid clip: $validStart, $validEnd. $action at index $iteration"
- Log.w(TAG, error)
}
}
- Log.d(TAG, "Final array size after processing: ${modifiedArray.size}")
+ Log.d(TAG, "=========================================================")
+ Log.d(TAG, "FINAL ARRAY SIZE AFTER PROCESSING: ${modifiedArray.size}")
modifiedArray
}
}
\ No newline at end of file
diff --git a/feature/player-shared/src/test/java/com/eva/player_shared/util/UpdateArrayViaConfigsTest.kt b/feature/player-shared/src/test/java/com/eva/player_shared/util/UpdateArrayViaConfigsTest.kt
new file mode 100644
index 00000000..8abb53c9
--- /dev/null
+++ b/feature/player-shared/src/test/java/com/eva/player_shared/util/UpdateArrayViaConfigsTest.kt
@@ -0,0 +1,133 @@
+package com.eva.player_shared.util
+
+import android.util.Log
+import com.eva.editor.domain.model.AudioClipConfig
+import com.eva.editor.domain.model.AudioEditAction
+import io.mockk.every
+import io.mockk.mockkStatic
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertArrayEquals
+import org.junit.Test
+import kotlin.test.BeforeTest
+import kotlin.test.assertEquals
+
+class UpdateArrayViaConfigsTest {
+
+ private val timePerBlock = 100
+
+ @BeforeTest
+ fun setup() {
+ mockkStatic(Log::class)
+ every { Log.d(any(), any()) } returns 0
+ every { Log.i(any(), any()) } returns 0
+ every { Log.e(any(), any()) } returns 0
+ }
+
+ @Test
+ fun `cut removes the middle section`() = runTest {
+ val original = FloatArray(10)
+ val configs = listOf(
+ AudioClipConfig(2 * timePerBlock, 5 * timePerBlock) to AudioEditAction.CUT
+ )
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+ val expectedSize = FloatArray(7)
+ assertArrayEquals(expectedSize, result, 0.0f)
+ }
+
+ @Test
+ fun `crop removes the boundary`() = runTest {
+ val original = FloatArray(10)
+ val configs = listOf(
+ AudioClipConfig(2 * timePerBlock, 5 * timePerBlock) to AudioEditAction.CROP
+ )
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+ val expected = FloatArray(3)
+ assertArrayEquals(expected, result, 0.0f)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `range is reveres should throw an exception`() = runTest {
+ val original = FloatArray(3)
+ val configs = listOf(
+ AudioClipConfig(5 * timePerBlock, 2 * timePerBlock) to AudioEditAction.CUT
+ )
+ original.updateArrayViaConfigs(configs, timePerBlock)
+ }
+
+ @Test
+ fun `empty configs don't change the array content`() = runTest {
+ val original = FloatArray(10)
+ val configs = emptyList>()
+
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+ assertArrayEquals(original, result, 0.0f)
+ }
+
+ @Test
+ fun `cut then cut then crop`() = runTest {
+ val original = FloatArray(10)
+ val configs = listOf(
+ AudioClipConfig(timePerBlock, 3 * timePerBlock) to AudioEditAction.CUT,
+ AudioClipConfig(5 * timePerBlock, 7 * timePerBlock) to AudioEditAction.CUT,
+ AudioClipConfig(timePerBlock, 4 * timePerBlock) to AudioEditAction.CROP
+ )
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+
+ val expected = FloatArray(3)
+ assertArrayEquals(expected, result, 0.0f)
+ }
+
+ @Test
+ fun `cut from front and cut from back`() = runTest {
+ val original = FloatArray(10) { it.toFloat() }
+
+ val configs = listOf(
+ AudioClipConfig(7 * timePerBlock, 10 * timePerBlock) to AudioEditAction.CUT,
+ AudioClipConfig(0, 3 * timePerBlock) to AudioEditAction.CUT
+ )
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+ val expected = floatArrayOf(3f, 4f, 5f, 6f)
+ assertArrayEquals(expected, result, 0.0f)
+ }
+
+ @Test
+ fun `cropping the array two times`() = runTest {
+ val original = FloatArray(10) { (it * 10).toFloat() }
+
+ val configs = listOf(
+ AudioClipConfig(2 * timePerBlock, 8 * timePerBlock) to AudioEditAction.CROP,
+ AudioClipConfig(timePerBlock, 4 * timePerBlock) to AudioEditAction.CROP
+ )
+
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+
+ val expected = floatArrayOf(30f, 40f, 50f)
+ assertArrayEquals(expected, result, 0.0f)
+ }
+
+ @Test
+ fun `combine a mix of cut and crop 1`() = runTest {
+ val original = floatArrayOf(1f, 2f, 3f, 4f, 5f)
+
+ val configs = listOf(
+ AudioClipConfig(0, 3 * timePerBlock) to AudioEditAction.CUT,
+ AudioClipConfig(0, timePerBlock) to AudioEditAction.CROP,
+ AudioClipConfig(0, timePerBlock) to AudioEditAction.CUT
+ )
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+ assertEquals(0, result.size)
+ }
+
+ @Test
+ fun `combine a mix of cut and crop 2`() = runTest {
+ val original = FloatArray(10) { it.toFloat() }
+
+ val configs = listOf(
+ AudioClipConfig(3 * timePerBlock, 7 * timePerBlock) to AudioEditAction.CUT,
+ AudioClipConfig(2 * timePerBlock, 4 * timePerBlock) to AudioEditAction.CROP,
+ )
+ val result = original.updateArrayViaConfigs(configs, timePerBlock)
+ val expected = floatArrayOf(2f, 7f)
+ assertArrayEquals(expected, result, 0f)
+ }
+}
\ No newline at end of file
diff --git a/feature/player/src/main/java/com/eva/feature_player/composable/AudioPlayerScreenContent.kt b/feature/player/src/main/java/com/eva/feature_player/composable/AudioPlayerScreenContent.kt
index d1571c55..269c3342 100644
--- a/feature/player/src/main/java/com/eva/feature_player/composable/AudioPlayerScreenContent.kt
+++ b/feature/player/src/main/java/com/eva/feature_player/composable/AudioPlayerScreenContent.kt
@@ -65,10 +65,13 @@ internal fun AudioPlayerScreenContent(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
- PlayerAmplitudeGraph(
+ PlayerAmplitudeGraph2(
trackData = trackData,
bookMarksTimeStamps = bookMarkTimeStamps,
graphData = waveforms,
+ isSwipeToScrollEnabled = true,
+ onSeek = { amount -> onPlayerEvents(PlayerEvents.OnSeekingPlayer(amount)) },
+ onSeekEnd = { onPlayerEvents(PlayerEvents.OnSeekEndPlayer) },
timelineFontFamily = DownloadableFonts.PLUS_CODE_LATIN_FONT_FAMILY,
modifier = Modifier.fillMaxWidth()
)
diff --git a/feature/player/src/main/java/com/eva/feature_player/composable/PlayerActionsAndSlider.kt b/feature/player/src/main/java/com/eva/feature_player/composable/PlayerActionsAndSlider.kt
index 8fa07142..a4406822 100644
--- a/feature/player/src/main/java/com/eva/feature_player/composable/PlayerActionsAndSlider.kt
+++ b/feature/player/src/main/java/com/eva/feature_player/composable/PlayerActionsAndSlider.kt
@@ -37,7 +37,8 @@ internal fun PlayerActionsAndSlider(
) {
PlayerTrackSlider2(
trackData = trackData,
- onSeekComplete = { amount -> onPlayerAction(PlayerEvents.OnSeekPlayer(amount)) },
+ onSeek = { amount -> onPlayerAction(PlayerEvents.OnSeekingPlayer(amount)) },
+ onSeekEnd = { onPlayerAction(PlayerEvents.OnSeekEndPlayer) },
enabled = isControllerSet
)
AudioPlayerActions(
diff --git a/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph.kt b/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph.kt
index e816a12b..0acbed97 100644
--- a/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph.kt
+++ b/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph.kt
@@ -134,6 +134,13 @@ private fun PlayerAmplitudeGraph(
}
}
+@Deprecated(
+ message = "This version of the amplitude graph is deprecated",
+ replaceWith = ReplaceWith(
+ "PlayerAmplitudeGraph2",
+ "com.eva.feature_player.composable.PlayerAmplitudeGraph2"
+ )
+)
@Composable
internal fun PlayerAmplitudeGraph(
trackData: () -> PlayerTrackData,
@@ -179,6 +186,7 @@ internal fun PlayerAmplitudeGraph(
@Preview
@Composable
+@Suppress("DEPRECATION")
private fun PlayerAmplitudeGraphPreview() = RecorderAppTheme {
PlayerAmplitudeGraph(
trackData = { PlayerPreviewFakes.FAKE_TRACK_DATA },
diff --git a/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph2.kt b/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph2.kt
new file mode 100644
index 00000000..6bed54d5
--- /dev/null
+++ b/feature/player/src/main/java/com/eva/feature_player/composable/PlayerAmplitudeGraph2.kt
@@ -0,0 +1,169 @@
+package com.eva.feature_player.composable
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFontFamilyResolver
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontSynthesis
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.resolveAsTypeface
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.eva.feature_player.views.PlayerAmplitudeGraph2View
+import com.eva.player.domain.model.PlayerTrackData
+import com.eva.player_shared.util.PlayRatio
+import com.eva.player_shared.util.PlayerGraphData
+import com.eva.ui.R
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.datetime.LocalTime
+import kotlin.time.Duration
+
+@Composable
+fun PlayerAmplitudeGraph2(
+ totalTrackDuration: Duration,
+ trackPlayRatio: PlayRatio,
+ graphData: PlayerGraphData,
+ modifier: Modifier = Modifier,
+ isSwipeToScrollEnabled: Boolean = false,
+ onSwipe: (ratio: Float) -> Unit = {},
+ onSwipeEnd: () -> Unit = {},
+ bookMarkTimeStamps: ImmutableList = persistentListOf(),
+ plotColor: Color = MaterialTheme.colorScheme.secondary,
+ trackPointerColor: Color = MaterialTheme.colorScheme.primary,
+ bookMarkColor: Color = MaterialTheme.colorScheme.tertiary,
+ timelineColor: Color = MaterialTheme.colorScheme.outline,
+ timelineColorVariant: Color = MaterialTheme.colorScheme.outlineVariant,
+ timelineTextStyle: TextStyle = MaterialTheme.typography.labelSmall,
+ timelineTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ contentPadding: PaddingValues = PaddingValues(20.dp),
+) {
+ val resolver = LocalFontFamilyResolver.current
+ val density = LocalDensity.current
+ val layoutDirection = LocalLayoutDirection.current
+
+ val typeFace by remember(timelineTextStyle) {
+ resolver.resolveAsTypeface(
+ fontFamily = timelineTextStyle.fontFamily ?: FontFamily.Monospace,
+ fontWeight = timelineTextStyle.fontWeight ?: FontWeight.Normal,
+ fontStyle = timelineTextStyle.fontStyle ?: FontStyle.Normal,
+ fontSynthesis = timelineTextStyle.fontSynthesis ?: FontSynthesis.None
+ )
+ }
+
+ AndroidView(
+ factory = { ctx ->
+ PlayerAmplitudeGraph2View(ctx).also { view ->
+ // colors and font
+ view.plotColor = plotColor.toArgb()
+ view.timelineColor = timelineColor.toArgb()
+ view.timelineColorVariant = timelineColorVariant.toArgb()
+ view.timelineTextColor = timelineTextColor.toArgb()
+ view.bookMarkColor = bookMarkColor.toArgb()
+ view.trackPointerColor = trackPointerColor.toArgb()
+ view.canvasBackground = containerColor.toArgb()
+ // text config
+ view.textTypeface = typeFace
+ with(density) {
+ view.textFontSizeAsPx = timelineTextStyle.fontSize.toPx()
+ view.setPadding(
+ contentPadding.calculateLeftPadding(layoutDirection).roundToPx(),
+ contentPadding.calculateTopPadding().roundToPx(),
+ contentPadding.calculateRightPadding(layoutDirection).roundToPx(),
+ contentPadding.calculateBottomPadding().roundToPx(),
+ )
+ }
+ view.isSwipeToScrollEnabled = isSwipeToScrollEnabled
+ // callbacks
+ view.onSwipeToChangeEnd(onSwipeEnd)
+ view.onSwipeToChangePlayPosition(onSwipe)
+ }
+ },
+ update = { view ->
+ // enable gesture detection
+ view.isSwipeToScrollEnabled = isSwipeToScrollEnabled
+ // if the view is visible then only show the values
+ if (view.isShown) {
+ view.onUpdateTrackDuration(totalTrackDuration)
+ view.onUpdateBookMarks(bookMarkTimeStamps)
+ view.onUpdatePlotColor(plotColor.toArgb())
+ view.onUpdatePlayRatio { trackPlayRatio() }
+ view.onUpdateGraphData { graphData() }
+ }
+ },
+ onRelease = { view -> view.cleanUp() },
+ modifier = modifier.defaultMinSize(minHeight = dimensionResource(id = R.dimen.line_graph_min_height))
+ )
+}
+
+@Composable
+internal fun PlayerAmplitudeGraph2(
+ trackData: () -> PlayerTrackData,
+ graphData: PlayerGraphData,
+ modifier: Modifier = Modifier,
+ bookMarksTimeStamps: ImmutableList = persistentListOf(),
+ plotColor: Color = MaterialTheme.colorScheme.secondary,
+ trackPointerColor: Color = MaterialTheme.colorScheme.primary,
+ bookMarkColor: Color = MaterialTheme.colorScheme.tertiary,
+ timelineColor: Color = MaterialTheme.colorScheme.outline,
+ timelineColorVariant: Color = MaterialTheme.colorScheme.outlineVariant,
+ timelineTextStyle: TextStyle = MaterialTheme.typography.labelSmall,
+ timelineTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ timelineFontFamily: FontFamily = FontFamily.Monospace,
+ isSwipeToScrollEnabled: Boolean = false,
+ onSeek: (Duration) -> Unit = {},
+ onSeekEnd: () -> Unit = {},
+ shape: Shape = MaterialTheme.shapes.small,
+ contentPadding: PaddingValues = PaddingValues(
+ horizontal = dimensionResource(id = R.dimen.graph_card_padding),
+ vertical = dimensionResource(id = R.dimen.graph_card_padding_other)
+ ),
+) {
+ val totalDuration by remember { derivedStateOf { trackData().total } }
+
+ Surface(
+ shape = shape,
+ color = containerColor,
+ modifier = modifier.aspectRatio(1.6f)
+ ) {
+ PlayerAmplitudeGraph2(
+ trackPlayRatio = { trackData().playRatio },
+ totalTrackDuration = totalDuration,
+ graphData = graphData,
+ bookMarkTimeStamps = bookMarksTimeStamps,
+ isSwipeToScrollEnabled = isSwipeToScrollEnabled,
+ onSwipe = { ratio ->
+ val duration = trackData().calculateSeekAmount(ratio)
+ onSeek(duration)
+ },
+ onSwipeEnd = onSeekEnd,
+ plotColor = plotColor,
+ trackPointerColor = trackPointerColor,
+ bookMarkColor = bookMarkColor,
+ timelineColor = timelineColor,
+ timelineColorVariant = timelineColorVariant,
+ timelineTextColor = timelineTextColor,
+ timelineTextStyle = timelineTextStyle.copy(fontFamily = timelineFontFamily),
+ contentPadding = contentPadding,
+ containerColor = containerColor,
+ )
+ }
+}
diff --git a/feature/player/src/main/java/com/eva/feature_player/state/PlayerEvents.kt b/feature/player/src/main/java/com/eva/feature_player/state/PlayerEvents.kt
index eb1c215e..2e1553a4 100644
--- a/feature/player/src/main/java/com/eva/feature_player/state/PlayerEvents.kt
+++ b/feature/player/src/main/java/com/eva/feature_player/state/PlayerEvents.kt
@@ -16,5 +16,7 @@ internal sealed interface PlayerEvents {
data class OnPlayerSpeedChange(val speed: PlayerPlayBackSpeed) : PlayerEvents
data class OnRepeatModeChange(val canRepeat: Boolean) : PlayerEvents
- data class OnSeekPlayer(val amount: Duration) : PlayerEvents
+
+ data class OnSeekingPlayer(val amount: Duration) : PlayerEvents
+ object OnSeekEndPlayer : PlayerEvents
}
\ No newline at end of file
diff --git a/feature/player/src/main/java/com/eva/feature_player/viewmodel/AudioPlayerViewModel.kt b/feature/player/src/main/java/com/eva/feature_player/viewmodel/AudioPlayerViewModel.kt
index 432b96d3..cffc8470 100644
--- a/feature/player/src/main/java/com/eva/feature_player/viewmodel/AudioPlayerViewModel.kt
+++ b/feature/player/src/main/java/com/eva/feature_player/viewmodel/AudioPlayerViewModel.kt
@@ -6,6 +6,7 @@ import com.eva.feature_player.state.PlayerEvents
import com.eva.player.domain.AudioFilePlayer
import com.eva.player.domain.model.PlayerMetaData
import com.eva.player.domain.model.PlayerTrackData
+import com.eva.player_shared.state.PlayerTrackUIState
import com.eva.recordings.domain.models.AudioFileModel
import com.eva.recordings.domain.provider.PlayerFileProvider
import com.eva.ui.viewmodel.AppViewModel
@@ -35,28 +36,33 @@ internal class AudioPlayerViewModel @AssistedInject constructor(
private val player: AudioFilePlayer,
) : AppViewModel() {
+ private val _trackController = PlayerTrackUIState()
+
private val _currentFile = MutableStateFlow(null)
private val _currentFileDistinctById = _currentFile
.filterNotNull()
.distinctUntilChangedBy { it.id }
- val playerMetaData = player.playerMetaDataFlow.stateIn(
- scope = viewModelScope,
- started = SharingStarted.Lazily,
- initialValue = PlayerMetaData()
- )
+ val playerMetaData = player.playerMetaDataFlow
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Lazily,
+ initialValue = PlayerMetaData()
+ )
- val isPlayerPlaying = player.isPlaying.stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = false
- )
+ val isPlayerPlaying = player.isPlaying
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Eagerly,
+ initialValue = false
+ )
- val trackData = player.trackInfoAsFlow.stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(5_000),
- initialValue = PlayerTrackData()
- )
+ val trackData = _trackController.controllablePlayerTrackData(player.trackInfoAsFlow)
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = PlayerTrackData()
+ )
val isControllerReady = player.isControllerReady
.onStart {
@@ -100,7 +106,17 @@ internal class AudioPlayerViewModel @AssistedInject constructor(
is PlayerEvents.OnPlayerSpeedChange -> player.setPlayBackSpeed(event.speed)
is PlayerEvents.OnRepeatModeChange -> player.setPlayLooping(event.canRepeat)
PlayerEvents.OnMutePlayer -> player.onMuteDevice()
- is PlayerEvents.OnSeekPlayer -> player.onSeekDuration(event.amount)
+
+ //seeking the player
+ is PlayerEvents.OnSeekingPlayer -> viewModelScope.launch {
+ _trackController.onSliderValueChange(event.amount)
+ }
+
+ PlayerEvents.OnSeekEndPlayer -> viewModelScope.launch {
+ _trackController.onInteractionFinished(
+ onSeekComplete = { player.onSeekDuration(it) },
+ )
+ }
}
}
diff --git a/feature/player/src/main/java/com/eva/feature_player/views/GraphScrollListener.kt b/feature/player/src/main/java/com/eva/feature_player/views/GraphScrollListener.kt
new file mode 100644
index 00000000..6815c627
--- /dev/null
+++ b/feature/player/src/main/java/com/eva/feature_player/views/GraphScrollListener.kt
@@ -0,0 +1,126 @@
+package com.eva.feature_player.views
+
+import android.util.Log
+import android.view.Choreographer
+import android.view.GestureDetector
+import android.view.MotionEvent
+import kotlin.math.abs
+
+private const val TAG = "GraphScrollListener"
+
+internal class GraphScrollListener(
+ private val totalContentWidthProvider: () -> Float,
+ private val onScrollStart: (Float) -> Unit = {},
+ private val onScrollEnd: () -> Unit = {},
+ private val onScroll: (Float) -> Unit,
+ private val flingEnabled: Boolean = true,
+) : GestureDetector.SimpleOnGestureListener(), Choreographer.FrameCallback {
+
+ private var _isScrolling = false
+ private var _isFlinging = false
+ private var _lastFrameTimeNanos = 0L
+
+ private var _scrollRatioInternal = 0.0f
+ private var _flingVelocity = 0f
+
+ private val hitBoundary: Boolean
+ get() = _scrollRatioInternal == 0f || _scrollRatioInternal == 1f
+
+ override fun onDown(e: MotionEvent): Boolean {
+ if (flingEnabled) stopFling()
+ return true
+ }
+
+ override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float)
+ : Boolean {
+ // Convert pixel distance to ratio change
+ val totalContentWidth = totalContentWidthProvider()
+ if (totalContentWidth <= 0f) return false
+ val deltaRatio = distanceX / totalContentWidth
+ _scrollRatioInternal = (_scrollRatioInternal + deltaRatio).coerceIn(0f, 1f)
+ onScroll(_scrollRatioInternal)
+ return true
+ }
+
+ override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float)
+ : Boolean {
+ if (flingEnabled) startFling(velocityX)
+ return true
+ }
+
+ override fun doFrame(frameTimeNanos: Long) {
+ if (flingEnabled && !_isFlinging) return
+
+ if (_lastFrameTimeNanos == 0L) {
+ _lastFrameTimeNanos = frameTimeNanos
+ // add the callback for the next frame
+ Choreographer.getInstance().postFrameCallback(this)
+ return
+ }
+
+ // Calculate delta time in seconds
+ val dTInSec = (frameTimeNanos - _lastFrameTimeNanos) / 1_000_000_000f
+ _lastFrameTimeNanos = frameTimeNanos
+
+ // Update scroll position
+ val previousRatio = _scrollRatioInternal
+ _scrollRatioInternal = (previousRatio + _flingVelocity * dTInSec).coerceIn(0f, 1f)
+
+ onScroll(_scrollRatioInternal)
+ _flingVelocity *= 0.98f
+
+ // slow down the fling velocity until it's too slow
+ if (abs(_flingVelocity) < 0.01f || hitBoundary) {
+ stopFling()
+ return
+ }
+ // On each frame we include the callback so convert the fling velocity to cancellations
+ Choreographer.getInstance().postFrameCallback(this)
+ }
+
+ private fun pxToRatioVelocity(velocityX: Float): Float {
+ val totalContentWidth = totalContentWidthProvider()
+ if (totalContentWidth <= 0f) return 0f
+ return -(velocityX / totalContentWidth) * 1.5f
+ }
+
+ private fun startFling(velocityX: Float) {
+ stopFling()
+
+ _flingVelocity = pxToRatioVelocity(velocityX)
+ _isFlinging = true
+ _lastFrameTimeNanos = 0L
+ Log.d(TAG, "FLING STARTED: VELOCITY :$velocityX")
+ Choreographer.getInstance().postFrameCallback(this)
+ }
+
+ private fun stopFling() {
+ if (!_isFlinging) return
+
+ _isFlinging = false
+ _flingVelocity = 0f
+ _lastFrameTimeNanos = 0L
+
+ Log.d(TAG, "FLING STOPPED")
+ // stop the frame callback
+ Choreographer.getInstance().removeFrameCallback(this)
+ // now call scroll end
+ onScrollEnd()
+ }
+
+ fun markScrollEnd() {
+ _isScrolling = false
+ Log.d(TAG, "SCROLL ENDED")
+ // don't call scroll end if flinging is on
+ if (_isFlinging) return
+ onScrollEnd()
+ }
+
+ fun markScrollStarted(positionX: Float, scrollRatio: Float) {
+ _isScrolling = true
+ stopFling()
+ Log.d(TAG, "SCROLL STARTED =$positionX")
+ onScrollStart(positionX)
+ _scrollRatioInternal = scrollRatio
+ }
+}
\ No newline at end of file
diff --git a/feature/player/src/main/java/com/eva/feature_player/views/PlayerAmplitudeGraph2View.kt b/feature/player/src/main/java/com/eva/feature_player/views/PlayerAmplitudeGraph2View.kt
new file mode 100644
index 00000000..e66d2541
--- /dev/null
+++ b/feature/player/src/main/java/com/eva/feature_player/views/PlayerAmplitudeGraph2View.kt
@@ -0,0 +1,423 @@
+package com.eva.feature_player.views
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.SurfaceTexture
+import android.graphics.Typeface
+import android.os.SystemClock
+import android.util.Log
+import android.util.TypedValue
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.TextureView
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.createBitmap
+import androidx.core.graphics.withClip
+import androidx.core.graphics.withTranslation
+import com.eva.ui.R
+import com.eva.utils.RecorderConstants
+import kotlinx.datetime.LocalTime
+import java.util.concurrent.CopyOnWriteArraySet
+import kotlin.concurrent.atomics.AtomicBoolean
+import kotlin.concurrent.atomics.ExperimentalAtomicApi
+import kotlin.math.roundToInt
+import kotlin.time.Duration
+
+private const val TAG = "PLAYER_AMPLITUDE_GRAPH_2"
+private const val TEXTURE_VIEW_TAG = "PLAYER_TEXTURE_LISTENER"
+
+@OptIn(ExperimentalAtomicApi::class)
+internal class PlayerAmplitudeGraph2View(context: Context) : TextureView(context),
+ TextureView.SurfaceTextureListener {
+
+ @Volatile
+ private var _renderThread: Thread? = null
+
+ @Volatile
+ private var _isThRunning = false
+
+ @Volatile
+ private var _isDataAvailable = false
+
+ // Cache for timeline
+ private var _timelineCacheBitmap: Bitmap? = null
+ private var _timelineCacheCanvas: Canvas? = null
+ private val _timelineCached = AtomicBoolean(false)
+
+ // Cache for graph data
+ private var _graphCacheBitmap: Bitmap? = null
+ private var _graphCacheCanvas: Canvas? = null
+
+ @Volatile
+ private var _cachedGraphDataSize = 0
+
+ // core draw components
+ @Volatile
+ private var _graphData: FloatArray = floatArrayOf()
+ private var _totalTrackDurationMillis: Long = 0L
+ private var _playRatio: Float = 0f
+ private val _bookMarkTimeStamps = CopyOnWriteArraySet()
+
+ // colors and font
+ var plotColor: Int = Color.WHITE
+ var bookMarkColor: Int = Color.WHITE
+ var timelineColor: Int = Color.WHITE
+ var timelineColorVariant: Int = Color.WHITE
+ var timelineTextColor: Int = Color.WHITE
+ var trackPointerColor: Int = Color.WHITE
+ var canvasBackground: Int = Color.BLACK
+ var textTypeface: Typeface = Typeface.MONOSPACE
+ var textFontSizeAsPx: Float = 12f
+
+ // resources
+ private val _bookMarkDrawable by lazy {
+ ResourcesCompat.getDrawable(resources, R.drawable.ic_bookmark, null)
+ }
+
+ // swipe to scroll
+ var isSwipeToScrollEnabled = false
+ private var _onPlayPosChangeViaScroll: ((Float) -> Unit)? = null
+ private var _onPlayPosChangeViaScrollEnd: (() -> Unit)? = null
+
+ private val _gestureDetectorListener = GraphScrollListener(
+ flingEnabled = false,
+ totalContentWidthProvider = { _timelineCacheBitmap?.width?.toFloat() ?: 0f },
+ onScrollEnd = { _onPlayPosChangeViaScrollEnd?.invoke() },
+ onScroll = { ratio -> _onPlayPosChangeViaScroll?.invoke(ratio) },
+ )
+
+ private val _gestureDetector by lazy { GestureDetector(context, _gestureDetectorListener) }
+
+ init {
+ surfaceTextureListener = this
+ isOpaque = true
+ isClickable = true
+ }
+
+ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
+ _renderThread = Thread(renderLoop(), "thGraphRenderer_")
+ _renderThread?.start()
+ Log.d(TEXTURE_VIEW_TAG, "RENDERER THREAD IS ACTIVE")
+ _isThRunning = true
+
+ // init bitmap cache from the given width and height
+ if (_graphCacheBitmap == null || _timelineCacheBitmap == null) {
+ Log.d(TEXTURE_VIEW_TAG, "RE-INITIATE CACHED BITMAPS")
+ initiateCacheBitmaps(width, height)
+ }
+ }
+
+ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
+ try {
+ _isThRunning = false
+ _renderThread?.join(1000)
+ } catch (e: Exception) {
+ Log.e(TEXTURE_VIEW_TAG, "THREAD CLEANUP", e)
+ }
+ Log.d(TEXTURE_VIEW_TAG, "RENDERER THREAD IS CLEANED! ${_renderThread?.state}")
+ // surface view is released
+ return true
+ }
+
+ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
+ Log.d(TEXTURE_VIEW_TAG, "SURFACE TEXTURE SIZE CHANGED")
+ // reinitiate bitmap cache as texture size changed
+ initiateCacheBitmaps(width, height)
+ }
+
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit
+
+ override fun performClick(): Boolean {
+ super.performClick()
+ return true
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (!isSwipeToScrollEnabled) return super.onTouchEvent(event)
+ // if swipe to scroll enabled then only allow the gesture detection
+ parent?.requestDisallowInterceptTouchEvent(true)
+ val handleEvents = _gestureDetector.onTouchEvent(event)
+ if (event.action == MotionEvent.ACTION_UP) performClick()
+ when (event.actionMasked) {
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> _gestureDetectorListener.markScrollEnd()
+ MotionEvent.ACTION_DOWN -> _gestureDetectorListener.markScrollStarted(
+ event.x,
+ _playRatio
+ )
+ }
+ // cancel touch events for the parent
+ return handleEvents || super.onTouchEvent(event)
+ }
+
+ private fun initiateCacheBitmaps(
+ width: Int,
+ height: Int,
+ resetTimelineCache: Boolean = true,
+ resetGraphCache: Boolean = true
+ ) {
+ if (width <= 0 || height <= 0) {
+ Log.w(TAG, "INVALID AREA | SIPPING UPDATE")
+ return
+ }
+
+ // evaluate the sizes
+ val maxSamples = RecorderConstants.RECORDER_AMPLITUDES_BUFFER_SIZE
+ val spikesWidth = width.toFloat() / RecorderConstants.RECORDER_AMPLITUDES_BUFFER_SIZE
+ val spikeSpace = (spikesWidth - dpToPx(1.5f)).let { amt ->
+ if (amt > 0f) amt else dpToPx(2.0f)
+ }
+ val blockWidth = spikesWidth + spikeSpace
+
+ val probableSampleSize = maxOf(_totalTrackDurationMillis / maxSamples, 10L)
+ val maxWidth = (probableSampleSize * blockWidth + paddingLeft).roundToInt()
+ .coerceAtLeast(width * 2)
+
+ if (resetTimelineCache) {
+ // recycle and clear the old bitmap
+ _timelineCacheBitmap?.recycle()
+ _timelineCacheBitmap = null
+ // build a timeline based on the probable size
+ _timelineCacheBitmap = createBitmap(maxWidth, height)
+ _timelineCacheCanvas = Canvas(_timelineCacheBitmap!!)
+ _timelineCached.store(false)
+ _timelineCacheBitmap?.apply {
+ Log.d(TAG, "TIMELINE CONTENT INVALIDATED NEW SIZE :${width} $height")
+ }
+ }
+
+ if (resetGraphCache) {
+ // clear the graph if any
+ _graphCacheBitmap?.recycle()
+ _graphCacheBitmap = null
+ // build an arbitrary graph cache based on the probable size
+ _graphCacheBitmap = createBitmap(maxWidth, height)
+ _graphCacheCanvas = Canvas(_graphCacheBitmap!!)
+ _cachedGraphDataSize = 0
+ _graphCacheBitmap?.apply {
+ Log.d(TAG, "GRAPH CONTENT INVALIDATED NEW SIZE :${width} $height")
+ }
+ }
+ }
+
+ private fun renderLoop(frameTimeMs: Long = 33L) = Runnable {
+ while (_isThRunning) {
+ val start = SystemClock.uptimeMillis()
+
+ if (surfaceTexture?.isReleased == true) break
+ // loop over the given canvas
+ if (_isDataAvailable) {
+ val canvas = lockCanvas() ?: continue
+ try {
+ canvas.drawFrame()
+ } finally {
+ _isDataAvailable = false
+ unlockCanvasAndPost(canvas)
+ }
+ }
+
+ val elapsed = SystemClock.uptimeMillis() - start
+ val sleep = frameTimeMs - elapsed
+ if (sleep <= 0) continue
+ try {
+ Thread.sleep(sleep)
+ } catch (_: InterruptedException) {
+ break
+ }
+ }
+ }
+
+ private fun Canvas.drawFrame() {
+ if (width <= 0 || height <= 0) return
+
+ drawColor(canvasBackground)
+
+ val spikesWidth = width.toFloat() / RecorderConstants.RECORDER_AMPLITUDES_BUFFER_SIZE
+ val spikeGap = (spikesWidth - dpToPx(1.5f)).let { amt ->
+ if (amt > 0f) amt else dpToPx(2.0f)
+ }
+
+ val probableSampleSize =
+ _totalTrackDurationMillis / RecorderConstants.RECORDER_AMPLITUDES_BUFFER_SIZE
+ val sampleSize = maxOf(_graphData.size.toLong(), probableSampleSize)
+ val totalSize = sampleSize * spikesWidth
+ val translate = (width * 0.5f - paddingLeft) - (totalSize * _playRatio)
+
+ // one time operation
+ if (!_timelineCached.load() && _timelineCacheCanvas != null) {
+ Log.d(TAG, "PREPARING TIME LINE")
+ _timelineCacheCanvas?.drawTimeLineWithBookMarks(
+ totalDurationInMillis = _totalTrackDurationMillis,
+ bookMarks = _bookMarkTimeStamps,
+ bookMarkDrawable = _bookMarkDrawable,
+ outlineColor = timelineColor,
+ outlineVariant = timelineColorVariant,
+ textColor = timelineTextColor,
+ bookMarkColor = bookMarkColor,
+ typeface = textTypeface,
+ spikesWidth = spikesWidth,
+ imageSize = dpToPx(14f),
+ textSizeInSp = textFontSizeAsPx,
+ dpToPx = ::dpToPx,
+ topPadding = paddingTop,
+ bottomPadding = paddingBottom,
+ leftPadding = paddingLeft
+ )
+ Log.d(TAG, "TIME TIMELINE DRAWING DONE!!")
+ _timelineCached.store(true)
+ }
+
+ // Draw cached timeline
+ withTranslation(x = translate) {
+ _timelineCacheBitmap?.let { bitmap ->
+ if (bitmap.isRecycled) return@withTranslation
+ drawBitmap(bitmap, 0f, 0f, null)
+ }
+ }
+
+ val currentDataSize = _graphData.size
+ val cachedSize = _cachedGraphDataSize
+ if (currentDataSize > cachedSize) {
+ // Extract only the new data
+ val newData = _graphData.sliceArray(cachedSize..
+ if (bitmap.isRecycled) return@withTranslation
+ drawBitmap(bitmap, 0f, 0f, null)
+ }
+ }
+ }
+
+ // Draw track pointer
+ drawTrackPointer(
+ xAxis = width * .5f,
+ color = trackPointerColor,
+ radius = spikesWidth,
+ strokeWidth = spikesWidth,
+ topPadding = paddingTop,
+ bottomPadding = paddingBottom
+ )
+ }
+
+ private fun dpToPx(dp: Float): Float =
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
+
+ /**
+ * Invalidate the timeline and graph.
+ * Graph also need to be invalidated as we modify the graph bitmap size too
+ * Now in special cases like when only the bookmarks are updated this is not need
+ */
+ private fun invalidateTimeline(redrawGraph: Boolean = true) {
+ _timelineCached.store(false)
+ _isDataAvailable = true
+
+ initiateCacheBitmaps(width, height, resetGraphCache = redrawGraph)
+ Log.i(TAG, "REDRAWING TIMELINE")
+ }
+
+ /**
+ * Invalidate only the graph.
+ * Graph is only invalidated no timeline changes
+ */
+ private fun resetGraphCache() {
+ _graphCacheCanvas?.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
+ _cachedGraphDataSize = 0
+ _isDataAvailable = true
+
+ // invalidate the graph but keep the timeline
+ initiateCacheBitmaps(width, height, resetTimelineCache = false)
+ Log.i(TAG, "REDRAWING GRAPH")
+ }
+
+ fun onUpdateTrackDuration(duration: Duration) {
+ val millis = duration.inWholeMilliseconds
+ if (millis == _totalTrackDurationMillis) return
+ _timelineCached.store(false)
+ _totalTrackDurationMillis = millis
+
+ // invalidate the timeline cache
+ invalidateTimeline()
+ Log.d(TAG, "TRACK DURATION UPDATED")
+ }
+
+ fun onUpdateBookMarks(bookMarks: List) {
+ val oldSize = _bookMarkTimeStamps.size
+ // reset the bookmarks
+ _bookMarkTimeStamps.clear()
+ val sortedList = bookMarks.map { it.toMillisecondOfDay() }.sorted()
+ _bookMarkTimeStamps.addAll(sortedList)
+
+ // Only invalidate if bookmarks actually changed redraw the timeline
+ if (_bookMarkTimeStamps.size != oldSize) {
+ invalidateTimeline(redrawGraph = false)
+ _isDataAvailable = true
+ Log.d(TAG, "BOOKMARKS SIZE CHANGED")
+ }
+ }
+
+ fun onUpdateGraphData(array: () -> FloatArray) {
+ _isDataAvailable = true
+ _graphData = array()
+ }
+
+ fun onUpdatePlotColor(color: Int) {
+ if (plotColor == color) return
+ plotColor = color
+ resetGraphCache()
+ Log.d(TAG, "PLOT COLOR UPDATED")
+ }
+
+ fun onUpdatePlayRatio(ratio: () -> Float) {
+ _playRatio = ratio()
+ }
+
+ fun onSwipeToChangeEnd(onScrollEndListener: () -> Unit) {
+ _onPlayPosChangeViaScrollEnd = onScrollEndListener
+ }
+
+ fun onSwipeToChangePlayPosition(onScrollListener: (Float) -> Unit) {
+ _onPlayPosChangeViaScroll = onScrollListener
+ }
+
+ fun cleanUp() {
+ if (_renderThread?.state != Thread.State.TERMINATED) {
+ _renderThread?.join()
+ }
+ _renderThread = null
+ // clean the bitmaps
+ _timelineCacheBitmap?.recycle()
+ _timelineCacheBitmap = null
+ _graphCacheBitmap?.recycle()
+ _graphCacheBitmap = null
+ Log.d(TEXTURE_VIEW_TAG, "CLEANUP CODE CALLED")
+
+ //clear callbacks
+ _onPlayPosChangeViaScrollEnd = null
+ _onPlayPosChangeViaScroll = null
+ Log.d(TAG, "CALLBACKS REMOVED")
+ }
+}
\ No newline at end of file
diff --git a/feature/player/src/main/java/com/eva/feature_player/views/ViewExtensions.kt b/feature/player/src/main/java/com/eva/feature_player/views/ViewExtensions.kt
new file mode 100644
index 00000000..8487cf5a
--- /dev/null
+++ b/feature/player/src/main/java/com/eva/feature_player/views/ViewExtensions.kt
@@ -0,0 +1,228 @@
+package com.eva.feature_player.views
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.text.TextPaint
+import androidx.core.graphics.withTranslation
+import java.util.Locale
+import kotlin.time.Duration.Companion.milliseconds
+
+fun Canvas.drawGraph(
+ waves: FloatArray,
+ spikesGap: Float = 2f,
+ spikesWidth: Float = 2f,
+ color: Int,
+ drawPoints: Boolean = true,
+ startIdx: Int = 0,
+ topPadding: Int = 0,
+ bottomPadding: Int = 0,
+ leftPadding: Int = 0,
+) {
+ val totalVPadding = topPadding + bottomPadding
+ val centerYAxis = (height - totalVPadding) * 0.5f
+ val paint = Paint().apply {
+ this.color = color
+ this.strokeWidth = spikesGap
+ strokeCap = Paint.Cap.ROUND
+ isAntiAlias = true
+ }
+
+ for ((idx, value) in waves.withIndex()) {
+ val actualIndex = startIdx + idx
+ val sizeFactor = value * .8f
+ val xAxis = leftPadding + spikesWidth * actualIndex
+ val startY = centerYAxis * (1 - sizeFactor)
+ val endY = centerYAxis * (1 + sizeFactor)
+
+ if (startY != endY) {
+ drawLine(
+ xAxis,
+ totalVPadding * .5f + startY,
+ xAxis,
+ totalVPadding * .5f + endY,
+ paint
+ )
+ } else if (drawPoints) {
+ drawCircle(
+ xAxis,
+ totalVPadding * .5f + centerYAxis,
+ spikesGap / 2f,
+ paint
+ )
+ }
+ }
+}
+
+fun Canvas.drawTimeLine(
+ totalDurationInMillis: Long,
+ textSizeInSp: Float,
+ outlineColor: Int = Color.GRAY,
+ outlineVariant: Int = Color.GRAY,
+ spikesWidth: Float = 2f,
+ strokeWidthThick: Float = 2f,
+ strokeWidthLight: Float = 1f,
+ textColor: Int = Color.BLACK,
+ sampleSize: Int = 100,
+ dpToPx: (Float) -> Float,
+ typeface: Typeface = Typeface.MONOSPACE,
+ topPadding: Int = 0,
+ bottomPadding: Int = 0,
+ leftPadding: Int = 0,
+) {
+ val durationAsMillis = (totalDurationInMillis + 2 * 1_000).toInt()
+ val spacing = spikesWidth / sampleSize
+
+ val paintThick = Paint().apply {
+ color = outlineColor
+ strokeWidth = strokeWidthThick
+ strokeCap = Paint.Cap.ROUND
+ isAntiAlias = true
+ }
+
+ val paintLight = Paint().apply {
+ color = outlineVariant
+ strokeWidth = strokeWidthLight
+ strokeCap = Paint.Cap.ROUND
+ isAntiAlias = true
+ }
+
+ val textPaint = TextPaint().apply {
+ color = textColor
+ this.textSize = textSizeInSp
+ isAntiAlias = true
+ textAlign = Paint.Align.CENTER
+ this.typeface = typeface
+ }
+
+ repeat(durationAsMillis) { millis ->
+ val xAxis = millis * spacing + leftPadding
+ if (millis % 2000 == 0) {
+ val time = millis.milliseconds
+ val minutes = time.inWholeMinutes
+ val seconds = (time.inWholeSeconds % 60)
+ val readable = String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
+
+ val textY = dpToPx(8f).unaryMinus() - (textPaint.descent() + textPaint.ascent()) / 2
+ drawText(readable, xAxis, topPadding + textY, textPaint)
+
+ drawLine(xAxis, topPadding.toFloat(), xAxis, topPadding + dpToPx(8f), paintThick)
+ drawLine(
+ xAxis,
+ height - dpToPx(8f) - bottomPadding,
+ xAxis,
+ height.toFloat() - bottomPadding,
+ paintThick
+ )
+ } else if (millis % 500 == 0) {
+ drawLine(xAxis, topPadding.toFloat(), xAxis, topPadding + dpToPx(4f), paintLight)
+ drawLine(
+ xAxis,
+ height - dpToPx(4f) - bottomPadding,
+ xAxis,
+ height.toFloat() - bottomPadding,
+ paintLight
+ )
+ }
+ }
+}
+
+fun Canvas.drawTimeLineWithBookMarks(
+ totalDurationInMillis: Long,
+ bookMarks: Iterable,
+ dpToPx: (Float) -> Float,
+ imageSize: Float = 20f,
+ textSizeInSp: Float = 16f,
+ bookMarkDrawable: Drawable? = null,
+ sampleSize: Int = 100,
+ outlineColor: Int = Color.WHITE,
+ outlineVariant: Int = Color.WHITE,
+ bookMarkColor: Int = Color.WHITE,
+ spikesWidth: Float = 2f,
+ strokeWidthThick: Float = 2f,
+ strokeWidthLight: Float = 1f,
+ bookMarkStokeWidth: Float = 2f,
+ textColor: Int = Color.WHITE,
+ typeface: Typeface = Typeface.MONOSPACE,
+ leftPadding: Int = 0,
+ topPadding: Int = 0,
+ bottomPadding: Int = 0,
+) {
+ drawTimeLine(
+ totalDurationInMillis = totalDurationInMillis,
+ sampleSize = sampleSize,
+ outlineColor = outlineColor,
+ outlineVariant = outlineVariant,
+ strokeWidthThick = strokeWidthThick,
+ strokeWidthLight = strokeWidthLight,
+ textColor = textColor,
+ textSizeInSp = textSizeInSp,
+ spikesWidth = spikesWidth,
+ dpToPx = dpToPx,
+ typeface = typeface,
+ topPadding = topPadding,
+ bottomPadding = bottomPadding,
+ leftPadding = leftPadding,
+ )
+
+ val spacing = spikesWidth / sampleSize
+
+ val bookMarkPaint = Paint().apply {
+ color = bookMarkColor
+ strokeWidth = bookMarkStokeWidth
+ strokeCap = Paint.Cap.ROUND
+ isAntiAlias = true
+ style = Paint.Style.FILL
+ }
+
+ bookMarks.forEach { timeInMillis ->
+ val xAxis = timeInMillis * spacing + leftPadding
+
+ // Draw vertical line
+ drawLine(
+ xAxis,
+ topPadding + dpToPx(2f),
+ xAxis,
+ height - dpToPx(2f) - bottomPadding,
+ bookMarkPaint
+ )
+ // Draw circle at top
+ drawCircle(xAxis, topPadding + dpToPx(2f), dpToPx(3f), bookMarkPaint)
+
+ // Draw bookmark icon
+ bookMarkDrawable?.let { drawable ->
+ withTranslation(xAxis - (imageSize / 2f), height + dpToPx(4f) - bottomPadding) {
+ drawable.setBounds(0, 0, imageSize.toInt(), imageSize.toInt())
+ drawable.setTint(bookMarkColor)
+ drawable.draw(this)
+ }
+ }
+ }
+}
+
+fun Canvas.drawTrackPointer(
+ xAxis: Float,
+ color: Int,
+ radius: Float = 1f,
+ strokeWidth: Float = 1f,
+ topPadding: Int = 0,
+ bottomPadding: Int = 0,
+) {
+ val paint = Paint().apply {
+ this.color = color
+ this.strokeWidth = strokeWidth
+ strokeCap = Paint.Cap.ROUND
+ isAntiAlias = true
+ style = Paint.Style.FILL
+ }
+
+ // Draw circles
+ drawCircle(xAxis, topPadding.toFloat(), radius, paint)
+ drawCircle(xAxis, (height - bottomPadding).toFloat(), radius, paint)
+
+ // Draw line
+ paint.style = Paint.Style.STROKE
+ drawLine(xAxis, topPadding.toFloat(), xAxis, (height - bottomPadding).toFloat(), paint)
+}
\ No newline at end of file
diff --git a/feature/widget/src/main/java/com/eva/feature_widget/recordings/composables/RecordingWidgetCard.kt b/feature/widget/src/main/java/com/eva/feature_widget/recordings/composables/RecordingWidgetCard.kt
index 4ed6c86e..c6309d1a 100644
--- a/feature/widget/src/main/java/com/eva/feature_widget/recordings/composables/RecordingWidgetCard.kt
+++ b/feature/widget/src/main/java/com/eva/feature_widget/recordings/composables/RecordingWidgetCard.kt
@@ -69,7 +69,7 @@ internal fun RecordingWidgetCard(
Spacer(modifier = GlanceModifier.width(8.dp))
Column(modifier = GlanceModifier.defaultWeight()) {
Text(
- text = model.title,
+ text = model.displayName,
style = TextStyle(
color = GlanceTheme.colors.onPrimaryContainer,
fontWeight = FontWeight.Medium,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b0e36a6f..685676ee 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.13.1"
+agp = "8.13.2"
concurrentFuturesKtx = "1.3.0"
datastore = "1.2.0"
coreSplashscreen = "1.2.0"
@@ -17,23 +17,23 @@ kotlinxCollectionsImmutable = "0.4.0"
kotlinxDatetime = "0.7.1"
kotlinxSerializationJson = "1.9.0"
lifecycleRuntimeKtx = "2.10.0"
-activityCompose = "1.12.1"
-composeBom = "2025.12.00"
-ksp = "2.3.0"
+activityCompose = "1.12.2"
+composeBom = "2025.12.01"
+ksp = "2.3.4"
hilt = "2.57.2"
-media3Common = "1.8.0"
+media3Common = "1.9.0"
navigationCompose = "2.9.6"
playServicesLocationVersion = "21.3.0"
roomCompiler = "2.8.4"
uiTextGoogleFonts = "1.10.0"
workRuntimeKtxVersion = "2.11.0"
hiltWork = "1.3.0"
-protobufJavalite = "4.33.1"
+protobufJavalite = "4.33.2"
protobuf_version = "0.9.5"
protobuf_gen_java_lite = "3.0.0"
materialIconExtended = "1.7.8"
moduleGrapher = "0.13.0"
-mockk = "1.14.6"
+mockk = "1.14.7"
coroutines = "1.10.2"
turbine_version = "1.2.1"
compileSdk = "36"
@@ -64,6 +64,7 @@ androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", versi
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Common" }
androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3Common" }
androidx-media3-effects = { module = "androidx.media3:media3-effect", version.ref = "media3Common" }
+androidx-media3-inspector = { module = "androidx.media3:media3-inspector", version.ref = "media3Common" }
androidx-media3-extractor = { module = "androidx.media3:media3-extractor", version.ref = "media3Common" }
androidx-media3-decoder = { module = "androidx.media3:media3-decoder", version.ref = "media3Common" }
androidx-media3-ui = { module = "androidx.media3:media3-ui-compose-material3", version.ref = "media3Common" }
diff --git a/stability_config.conf b/stability_config.conf
index cdea9567..430e9437 100644
--- a/stability_config.conf
+++ b/stability_config.conf
@@ -7,6 +7,7 @@ com.eva.bookmarks.domain.AudioBookmarkModel
com.eva.categories.domain.models.*
com.eva.datastore.domain.models.*
com.eva.datastore.domain.enums.*
+com.eva.editor.domain.model.*
com.eva.interactions.domain.enums.*
com.eva.location.domain.BaseLocationModel
com.eva.player.domain.model.*