151 Commits

Author SHA1 Message Date
Ghosuto
7062062d3a dolby: Externalize UI strings for localization support
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-18 02:34:35 +09:00
Ghosuto
935738fd2f dolby: Allow hal_dms_default to connect to property socket
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-18 02:34:34 +09:00
Ghosuto
2653104313 dolby: Prevent profile reset in AppProfileMonitorService
- add logging for profile change debugging

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-18 02:33:56 +09:00
Ghosuto
6742a7510b dolby: Simple name better 2026-01-18 02:33:56 +09:00
Ghosuto
814f200b96 dolby: Add Credits & Contributors dialog
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-18 02:33:50 +09:00
Ghosuto
e43d068c08 dolby: Use accent color for dolby icon bg 2026-01-18 02:33:11 +09:00
Ghosuto
23e4a872b4 dolby: Switch to material 3 expressive color specs
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-18 02:32:53 +09:00
Ghosuto
57a0f17b42 dolby: Add delete option for presets in import/export menu
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:10 +09:00
Ghosuto
00f10ec799 dolby: Fix preset not loading with its original band mode 2026-01-17 22:01:10 +09:00
Ghosuto
5e8cd0042a dolby: Fix volume leveler not reading config
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:10 +09:00
Ghosuto
11504da95e dolby: Move notification listener permission card to home screen
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:10 +09:00
Ghosuto
a0ef8bcdad dolby: Restore all profile settings on device boot
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:10 +09:00
Ghosuto
bc9f70853b dolby: Fix navbar style
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:09 +09:00
Ghosuto
89e1f6d7fc dolby: Add headphone-only mode for per-app audio profiles
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:09 +09:00
Ghosuto
08f2846211 dolby: Saftey threads and memory leak fix
- race condition fix
- preset caching

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:09 +09:00
Ghosuto
14fd4056ad dolby: Add lifecycle management and resource cleanup
- resource cleanup and state validation

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:09 +09:00
Ghosuto
6920567e70 dolby: Implement Notification Listener for always-on Dolby service
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-17 22:01:09 +09:00
Ghosuto
933f49f3ff dolby: Remove legacy vibrator fallback check 2026-01-17 22:01:09 +09:00
Ghosuto
868eefca14 dolby: Use Material 3 Expressive motion specs 2026-01-17 22:01:09 +09:00
hiroshi
b84590682a dolby: sepolicy: allow hal_dms_default to read vendor_audio_prop
relevant log:
[  285.012368] type=1400 audit(1765015259.463:23453): avc:  denied  { read } for  comm="vendor.dolby.ha" name="u:object_r:vendor_audio_prop:s0" dev="tmpfs" ino=429 scontext=u:r:hal_dms_default:s0 tcontext=u:object_r:vendor_audio_prop:s0 tclass=file
2026-01-17 22:01:09 +09:00
Ghosuto
a329cb2b27 dolby: Fix background color in light mode 2026-01-09 11:27:37 +05:30
pabloescobar-reborn
d054d08e26 Revert "dolby: Update Dolby icon"
This reverts commit 28ec608d56.
2026-01-08 08:45:36 +00:00
Ghosuto
278b17ead3 dolby: Fix Band mode compatibility and array bounds crashes
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 12:21:01 +00:00
Ghosuto
95281a58ab dolby: Prevent editing presets with mismatched band modes
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 12:21:01 +00:00
Ghosuto
f64689fbde dolby: Fix bottom navbar overlap issue
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:54:30 +00:00
Ghosuto
700575a500 dolby: Add tile squish effect
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:54:30 +00:00
Ghosuto
96341f3d7c dolby: Enhance Switch UI with animated icons
- from https://github.com/Lunaris-AOSP/packages_apps_GameSpace/blob/16/app/src/main/java/io/chaldeaprjkt/gamespace/ui/components/SettingsComponents.kt

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:54:30 +00:00
Ghosuto
b906be38fa dolby: Use neutral color for settings bg
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:53:53 +00:00
Ghosuto
ecd925b89d dolby: Remove unnecessary GZIP compression from preset import/export
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:11:53 +00:00
Ghosuto
9c334b7c91 dolby: Add missing icon for Preset and Equalizer View
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:11:53 +00:00
Ghosuto
6aebff870a dolby: Extend equalizer frequency range and add configurable band modes
- Match with dex
- Use slider to control higher bands

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:11:53 +00:00
Ghosuto
eeb104fd9a dolby: Introduce preset import/export support
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-07 04:11:51 +00:00
Ghosuto
c4c56687b2 dolby: Implement haptic feedback
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:53:45 +05:30
Ghosuto
0d664c26ee dolby: Only show toast for apps with assigned profiles
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:53:15 +05:30
Ghosuto
9481b3b8cf dolby: Implement per-app profile management
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:52:38 +05:30
Ghosuto
873fdd3f9a dolby: Add 3 new profiles
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:44:44 +05:30
Ghosuto
f2c74ae5bb dolby: Fix preset now switching when switch profile
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:44:44 +05:30
Ghosuto
28ec608d56 dolby: Update Dolby icon
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:44:19 +05:30
Ghosuto
d0e960bcd5 dolby: Fix equ curve bright bg color
- Too bright in some monet colors

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:40:58 +05:30
Ghosuto
a318b2211a dolby: Add view mode toggle for equalizer interface
- Switch between og slider and curve

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:40:58 +05:30
Ghosuto
c39a6aed6c dolby: Improve equalizer curve touch responsiveness
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:40:58 +05:30
Ghosuto
267c2fac6f dolby: Fix equalizer preset detection and matching
fix - 69fdd6cf4f

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:40:58 +05:30
Ghosuto
3aa7258551 dolby: Improve bottom navbar design
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-05 10:40:35 +05:30
Ghosuto
ed4b480e2a dolby: Dynamic tile and switch shape
- like a16 compose qs tiles

Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-03 03:48:27 +00:00
Ghosuto
be2b3ef445 dolby: Fix equ interface
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-03 03:48:27 +00:00
Ghosuto
9bba67c6bb dolby: Redesign dolby interface
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 21:03:05 +05:30
Ghosuto
5c91dcd7c9 dolby: Add Carousel Profile Selector
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 21:02:53 +05:30
Ghosuto
0bdd102473 dolby: Added border stroke to selected profile option
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 21:02:38 +05:30
Ghosuto
bc8daa97f6 dolby: Switch to interactive frequency response curve 2026-01-02 21:02:20 +05:30
kenway214
3b85fb287e dolby: add selectable bass curves and curve-based EQ processing
Signed-off-by: kenway214 <kenway214@outlook.com>
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 20:59:37 +05:30
kenway214
32b1e6aa87 dolby: enhance audio controls with treble tuning
Signed-off-by: kenway214 <kenway214@outlook.com>
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 20:59:37 +05:30
kenway214
c0e14ef659 dolby: add new EQ presets and extend gain range to ±15dB
Signed-off-by: kenway214 <kenway214@outlook.com>
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 20:59:37 +05:30
Ghosuto
336e88ed9c dolby: Add bass control slider
- Taken from peridot packages_apps_XiaomiDolby

Co-authored-by: kenway214 <kenway214@outlook.com>
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 11:23:51 +05:30
Ghosuto
5db10db2b1 dolby: Complete rewrite DolbyManager in Jetpack Compose
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
Signed-off-by: Pabloescobar-reborn <pabloescobarreborn07@gmail.com>
2026-01-02 11:23:13 +05:30
Ghosuto
13c32ef7b4 dolby: Use new material expressive design for settings preferences 2026-01-02 08:17:13 +05:30
Ghosuto
0e03fffcdb dolby: add missing software codec Attribute
Signed-off-by: Ghosuto <clash.raja10@gmail.com>
2026-01-02 08:16:52 +05:30
Yogesh
fe94e80ffa Merge pull request #1 from DqrKnzx/16
dolby: fix attributes and tags in dax_default.xml
2025-11-29 00:41:08 +05:30
DqrKnzx
944a468640 dolby: fix attributes and tags in dax_default.xml
- Typos prevented some audio settings from being applied (gain_left, virtual bass).

Signed-off-by: DqrKnzx <DavidYonatan9@hotmail.com>
2025-11-28 19:00:59 +00:00
chaitanya
63bcd895df dolby: fixup voice profile 2025-09-07 15:55:34 +00:00
chaitanyakm
de23ccc185 dolby: update dax-default.xml 2025-09-07 15:55:25 +00:00
Adithya R
088743af43 dolby: Do not conditionally disable prefs on speaker
We have separate toggles for speaker and headphone virtualizer so
this doesn't really make sense.

Change-Id: I95406f2bb1815146aa52fb49541ffd60541ca05f
2025-09-02 02:26:10 +00:00
Adithya R
0353d1e21f dolby: Allow force disabling volume leveler
Change-Id: Ib70e1f27d8280c8b4ec456e4feaa30522fdc5121
2025-09-02 02:26:10 +00:00
Adithya R
fbb15cb28b dolby: Avoid hardcoded strings for custom preset
Change-Id: I6a2b16cb67bc0bf4def9fbe17aac30664e62f4d7
2025-09-02 02:26:10 +00:00
Adithya R
67eed6a82c dolby: Move to device encrypted storage
Change-Id: I9135755bb79acb2a9f74b040403973b056941827
2025-09-02 02:26:10 +00:00
Adithya R
40fe6856d7 dolby: Move effect strength settings to sliders
Stereo widening and dialogue enhancer.

Change-Id: I3f2eb7d01c11f90e9080d22f8f390f8adddcba14
2025-09-02 02:26:10 +00:00
Adithya R
60ed7e0863 dolby: Hide advanced settings in dynamic profile
This is in line with DaxUI.

Change-Id: I6ea2657c86e5c4cfb1aa6f78d651b330f56ae921
2025-09-02 02:26:10 +00:00
Adithya R
858790df5d dolby: Add a menu button to reset all profiles
Change-Id: I16c4f7e06279bdcb1a1f2977ce48d2487caecac3
2025-09-02 02:26:10 +00:00
Adithya R
31d7175068 fixup! dolby: Add intelligent equalizer setting 2025-09-02 02:26:10 +00:00
Adithya R
d40e763990 fixup! dolby: Use index of value for selecting ieq icon 2025-09-02 02:26:10 +00:00
Pranav Vashi
4920046dbe dolby: Fix compilation error in TooltipIconButton
Fixes: TooltipIconButton.kt:30:44: error: unresolved reference: rememberPlainTooltipPositionProvider

Change-Id: I7554622a406f4a56dc8471a39cbd4ad73c0daede
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
2025-09-02 02:26:10 +00:00
Adithya R
c3f3a11aeb dolby: Use index of value for selecting ieq icon
Otherwise if we alter value ids (dolby_ieq_values) via overlay,
the wrong icon is displayed since the value differs from index.

Change-Id: I4ec55719a31cb22777069b62fa550eadd3d946fb
2025-09-02 02:26:10 +00:00
Adithya R
753b34ec59 dolby: Allow disabling stereo widening levels
On newer dolby tunings, this is unsupported. It can only be turned
on/off (headphone virtualization).

Change-Id: Iffbffd81bb22510fa10418f9247ce89c0513b09e
2025-09-02 02:26:10 +00:00
Adithya R
05ca7c6837 dolby: Enable bass enhancer on loudspeaker too
It is a subtle but noticeable effect.

Change-Id: I385c8474f2423c7496423716448f7480cd6acbf3
2025-09-02 02:26:10 +00:00
Adithya R
a4cd52a83c dolby: Properly update profile preference summary
When profile is changed to known from unknown, the preference didn't
get updated.

Change-Id: Ic5e41f8c853187272d00178c7de02551f16350f9
2025-09-02 02:26:10 +00:00
Adithya R
b8e001e5eb dolby: Add support for Custom profile
This appears to be present in all devices' dolby configurations.

Change-Id: I651a5f0f64144bbda6de863989043f9daf40b213
2025-09-02 02:26:10 +00:00
Fabian Leutenegger
440bb655d2 dolby: Update EqualizerScreen background color for 15
Switch to MaterialTheme settingsBackground for EqualizerScreen background color

Change-Id: I546e3528814276eb857a650cb6c173d914550fb5
2025-09-02 02:26:10 +00:00
Pranav Vashi
b27a259975 dolby: Remove deprecated PlainTooltipBox
Change-Id: I70ffff5ba30c5eeaff431e46c82eaf05d46e4cb0
2025-09-02 02:26:10 +00:00
basamaryan
d43556f426 dolby: Fix build with kotlinc 1.9.0
Change-Id: I4f9fdc9d25eb57240612cff1b3bef3663014f9a8
Signed-off-by: Adithya R <gh0strider.2k18.reborn@gmail.com>
2025-09-02 02:26:10 +00:00
Michael Bestas
939088422f dolby: Convert to SwitchPreferenceCompat
Change-Id: Ic1cbaba37d499da1855af9c3930f2df426e2d3af
2025-09-02 02:26:10 +00:00
Chaohui Wang
600851e820 dolby: Migrate to CompoundButton.OnCheckedChangeListener
Switch and SwitchCompat are both CompoundButton.

Using CompoundButton in Java will helps migration in the future.

Bug: 306658427 | AOSP | AOSP
Test: manual - check Settings pages
Test: m RunSettingsLibRoboTests
Change-Id: I85a70d4c504d8584030ea4a058f30d74206ab835
2025-09-02 02:26:10 +00:00
Fabian Leutenegger
ca33c4dd78 dolby: Replace widget.R with collapsingtoolbar.R
Change-Id: I725a3bdb06f2a697791b882c5709f59b6089ba15
2025-09-02 02:26:10 +00:00
Adithya R
6db983af3e dolby: Make sure to persist value after toggling QS tile
Toggling the switch pref automatically sets the shared pref for us, but
toggling the QS tile does no such thing so we gotta do it ourselves.

Change-Id: Iac881ed654bf4eb76b111fc87667f16476d11522
2025-09-02 02:26:10 +00:00
Adithya R
318f0ea617 dolby: Add intelligent equalizer setting
Move preference-related classes to a new package while we're at it,
to reduce code clutter.

Change-Id: I2430e8ab9b6758503ce1777ec985a3e400b55b8e
2025-09-02 02:26:10 +00:00
Adithya R
b44187ef8f dolby: Introduce graphical equalizer
Squashed:

dolby: Refresh preset name on main screen

Change-Id: I96783e2a03c384f031787f4cc9140f7d64dadb2f
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>

Change-Id: I38ee6ce594e5671af42afc3d4bf0f004329482b9
2025-09-02 02:26:10 +00:00
Pranav Vashi
14efd187f8 dolby: Add launcher icon
Change-Id: I4d36842ca96048f9b55604d66cc7741759d657f3
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
[adithya2306: Add monochrome icon as well]
Signed-off-by: Adithya R <gh0strider.2k18.reborn@gmail.com>
2025-09-02 02:26:10 +00:00
Adithya R
4f1393f337 fixup! dolby: Restore all settings upon bootup
Stereo widening dependency on virtualizer was accidentally removed.

Change-Id: I9b1e35aef5037935af3dc18a303408e2a81ca635
2025-09-02 02:26:10 +00:00
Adithya R
4774c82a5b dolby: Restore current profile _after_ resetting profiles
Ensure to end the onBootCompleted routine with the correct profile set.

Change-Id: I2d5f74a7c0145af2f9d064cd98fa2dc70e5a7acd
2025-09-02 02:26:10 +00:00
Adithya R
7a1ef573d6 dolby: Do not set volume leveler amount
This value is set to zero in almost every known dax-default.xml,
including ours.
DaxService also doesn't mess with this value, instead only sets
VolumeLevelerEnabled.

Change-Id: Ib944728d478cff58aebc4f47128bcd5fe32ff9f6
2025-09-02 02:26:10 +00:00
Adithya R
e92669bd12 dolby: Restore all settings upon bootup
Dolby often messes up restoring profile-specific settings after a reboot.
"Fine. I'll do it myself."

Change-Id: Ic255c6922eabae0b522c05110f87e2c10a97fb6c
2025-09-02 02:26:10 +00:00
Adithya R
586755d10b dolby: Rewrite in Kotlin
Some cleanup and restructuring while we're at it.

Change-Id: I2f1fc53c202d91421c7b6af68c814c25398a62e4
2025-09-02 02:26:10 +00:00
Adithya R
863fce3d24 dolby: Revert "Re-enable speaker virtualization after bootup"
No longer necessary

Change-Id: Iac820eafa71ea3e4ccaad2bfa0fb76c37279a22a
2025-09-02 02:26:10 +00:00
Adithya R
5ab1d968be dolby: Introduce Dolby Atmos
Moved from marble/sm8450-common

History:

commit 82fe03168c0402e4cb10d25859c3b398c0ef654a
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Thu Mar 21 21:35:36 2024 +0530

    marble: parts: Restore dolby profile on audio changes

    Something keeps resetting back at random times, from what I observed,
    after resuming media or on a device change, lets workaround that.

    Change-Id: Id065f2482636194655c2399f0c35ad56b8e7a29d

commit c4400bd1326f65aeac1d0f26bb830ce7fd079773
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Fri Feb 2 09:29:08 2024 +0530

    marble: parts/keyhandler: Guard debug logging

    Change-Id: I246941f26cd1f71b696eb3c996794c9baa5dbc00

commit f11b70a98a11d0b89673d73002996aed9f11fbd7
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Sun Dec 31 20:36:52 2023 +0530

    marble: parts: Re-enable speaker virtualization after bootup

    For whatever reason, speaker virtualization isn't automatically
    restored at bootup unlike the other parameters. It was reported to be
    fixed by connecting and disconnecting headphones or disabling and
    enabling the toggle, so let's just automate that at bootup.

commit abcff4fb947c89b69c1d25bd290fd91b7873af6a
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Fri Oct 20 06:49:19 2023 +0530

    marble: parts: Implement profile-specific Dolby settings

    Some refactoring and cleanup while we're at it.

commit dc54f9ddeff212d017b0cba16e56516e99335bb3
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Mon Oct 9 21:58:58 2023 +0530

    marble: parts: Remove play/pause hack while toggling Dolby

    Not required with/fixed by:
    35217: audioflinger: Do not allow DAP effect to be suspended | https://gerrit.aospa.co/c/AOSPA/android_frameworks_av/+/35217

commit dd2acc8e0c10d05f86ff229412cc9f72ea242b44
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Wed Sep 13 21:41:20 2023 +0530

    marble: parts: Set proper summary for dolby settings

    Show the current status in Settings > Sound as well as the QS tile.

commit 92d341ba3d22f323eded525487db4289d6edc0fe
Author: Fabian Leutenegger <fabian.leutenegger@bluewin.ch>
Date:   Fri Aug 25 10:26:53 2023 +0200

    marble: parts: Always refresh playback if status changed

     * otherwise dolby would stay active even if you disable its setting

    Change-Id: If59d8081fa12da2aa67e5149db97965c0805d76e

commit b1944744649b6fddcb7bc3864b92f298b6e78821
Author: Adithya R <gh0strider.2k18.reborn@gmail.com>
Date:   Mon Aug 21 13:21:18 2023 +0530

    marble: parts: Introduce Dolby Atmos

    Based on existing dirac implementation and observing stock
    sound effects app and daxservice.

    Thanks to jhenrique09 for the hack from old dirac parts
    "Pause/play music stream to get effects applied".

    TODO: bring back misound (same as stock)

    Co-authored-by: Henrique Silva <jhenrique09.mcz@hotmail.com>

Change-Id: I79841c045fe7b92c438177916f756faab72ff0e9
2025-09-02 02:26:10 +00:00
swiitchOFF
1d06c0bd9d dolby: Yeet Motorola Dolby Interface 2025-09-02 02:25:32 +00:00
swiitchOFF
708a47bda4 dolby: Update readme 2025-08-31 04:31:31 +00:00
New Author Name
892caadbe7 dolby: Switch to AOSPA Dolby Atmos
* Based on Paranoid Android Vauxite (7) [03/08/2025]

Co-Authored-By: Adithya R <gh0strider.2k18.reborn@gmail.com>
Signed-off-by: swiitchOFF <120115258+swiitchOFF@users.noreply.github.com>
2025-08-31 04:31:06 +00:00
swiitchOFF
e7dbf443a2 dolby: Update readme 2025-08-31 08:49:41 +05:30
someone5678
6b1bb6b1e8 dolby: Create missing dolby related directory 2025-08-31 08:49:41 +05:30
keosh
0475f7ef37 FCM: moved to dt to fix kernel target issue 2025-08-29 20:27:43 +05:30
pabloescobar-reborn
0e837f8e94 Revert "dolby: Implement DSPVolumeSynchronizer"
This reverts commit 23662b74ac.
2025-08-28 15:52:17 +00:00
Pabloescobar-reborn
263ba11385 dolby: Rework on dax-default 2025-08-28 21:12:21 +05:30
swiitchOFF
54da022d03 dolby: configs: Rework on dax-default
* It's 😍 on my device and I'm use too with it xD
* From e8daa705f0c0cc3e59da9242e84d15cec8cd1d7d
2025-08-28 09:41:03 +00:00
swiitchOFF
4f911ddffa dolby: Update MotoDolbyDax3.apk [White icon] 2025-08-28 09:41:03 +00:00
swiitchOFF
a4f8660263 dolby: Patch vendor.dolby.media.c2@1.0-service with libshim_dolby
08-25 13:26:29.598  1006  1006 F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 1006 (vendor.dolby.me), pid 1006 (vendor.dolby.me)
08-25 13:26:29.731  3084  3084 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
08-25 13:26:29.731  3084  3084 F DEBUG   : crDroid Version: '11.8'
08-25 13:26:29.731  3084  3084 F DEBUG   : Build fingerprint: 'Xiaomi/vili/vili:14/UKQ1.231207.002/V816.0.10.0.UKDMIXM:user/release-keys'
08-25 13:26:29.731  3084  3084 F DEBUG   : Revision: '0'
08-25 13:26:29.731  3084  3084 F DEBUG   : ABI: 'arm64'
08-25 13:26:29.731  3084  3084 F DEBUG   : Timestamp: 2025-08-25 13:26:29.665897496+0530
08-25 13:26:29.731  3084  3084 F DEBUG   : Process uptime: 10s
08-25 13:26:29.731  3084  3084 F DEBUG   : Cmdline: /vendor/bin/hw/vendor.dolby.media.c2@1.0-service
08-25 13:26:29.731  3084  3084 F DEBUG   : pid: 1006, tid: 1006, name: vendor.dolby.me  >>> /vendor/bin/hw/vendor.dolby.media.c2@1.0-service <<<
08-25 13:26:29.731  3084  3084 F DEBUG   : uid: 1046
08-25 13:26:29.731  3084  3084 F DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000000
08-25 13:26:29.731  3084  3084 F DEBUG   : Cause: null pointer dereference
08-25 13:26:29.731  3084  3084 F DEBUG   :     x0  b400006f05b52fc0  x1  0000007fc905ed10  x2  0000006f45b59a90  x3  0000007fc905ecd8
08-25 13:26:29.731  3084  3084 F DEBUG   :     x4  0000000000000028  x5  0200006ef5b52470  x6  0000000000000000  x7  0000000000000000
08-25 13:26:29.731  3084  3084 F DEBUG   :     x8  00000000ffffffff  x9  00000000eff00000  x10 0000006fe69e4fa4  x11 0000000012b97fad
08-25 13:26:29.731  3084  3084 F DEBUG   :     x12 0000000000028003  x13 b400006ef5b528e0  x14 0000000000000024  x15 000000000000005f
08-25 13:26:29.731  3084  3084 F DEBUG   :     x16 0000006fe5ebd230  x17 0000006fede1ca84  x18 0000006fef730000  x19 0000000000000000
08-25 13:26:29.731  3084  3084 F DEBUG   :     x20 b400006f05b52eb0  x21 0000006d55852090  x22 0000006fef2fff00  x23 0000000000000000
08-25 13:26:29.731  3084  3084 F DEBUG   :     x24 0000006fef2fff00  x25 b400006d55b59140  x26 0000000000000001  x27 0000000000000000
08-25 13:26:29.731  3084  3084 F DEBUG   :     x28 0000000000000000  x29 0000007fc905ece0
08-25 13:26:29.731  3084  3084 F DEBUG   :     lr  0000006fe5e915f4  sp  0000007fc905ece0  pc  0000006fede1caa0  pst 0000000080001000
08-25 13:26:29.731  3084  3084 F DEBUG   : 7 total frames
08-25 13:26:29.731  3084  3084 F DEBUG   : backtrace:
08-25 13:26:29.731  3084  3084 F DEBUG   :       #00 pc 0000000000010aa0  /vendor/lib64/libutils.so (android::RefBase::decStrong(void const*) const+28) (BuildId: 2fd112063e39cb6f651b185f9e6234f2)
08-25 13:26:29.731  3084  3084 F DEBUG   :       #01 pc 00000000000515f0  /vendor/lib64/android.hardware.media.c2@1.0.so (android::hardware::media::c2::V1_0::BnHwComponentStore::_hidl_createInterface(android::hidl::base::V1_0::BnHwBase*, android::hardware::Parcel const&, android::hardware::Parcel*, std::__1::function<void (android::hardware::Parcel&)>)+376) (BuildId: f0d25f49428387d313920490e7ec7c8c)
08-25 13:26:29.731  3084  3084 F DEBUG   :       #02 pc 00000000000525f0  /vendor/lib64/android.hardware.media.c2@1.0.so (android::hardware::media::c2::V1_0::BnHwComponentStore::onTransact(unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int, std::__1::function<void (android::hardware::Parcel&)>)+1004) (BuildId: f0d25f49428387d313920490e7ec7c8c)
08-25 13:26:29.731  3084  3084 F DEBUG   :       #03 pc 000000000008ab7c  /vendor/lib64/libhidlbase.so (android::hardware::BHwBinder::transact(unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int, std::__1::function<void (android::hardware::Parcel&)>)+92) (BuildId: 1d845c84de7e27fc2c78f04ee8f4aae0)
08-25 13:26:29.731  3084  3084 F DEBUG   :       #04 pc 0000000000042eac  /vendor/lib64/libhidlbase.so (android::hardware::IPCThreadState::joinThreadPool(bool)+1344) (BuildId: 1d845c84de7e27fc2c78f04ee8f4aae0)
08-25 13:26:29.731  3084  3084 F DEBUG   :       #05 pc 000000000000124c  /vendor/bin/hw/vendor.dolby.media.c2@1.0-service (main+500) (BuildId: 394e52c5b5064b9f64c91bd2d615f078)
08-25 13:26:29.731  3084  3084 F DEBUG   :       #06 pc 0000000000055160  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+116) (BuildId: b310372d5ddccd24fca9f3a465bff80c)

Co-Authored-By: ZiadTamer <ziadtamer150@gmail.com>
Signed-off-by: swiitchOFF <120115258+swiitchOFF@users.noreply.github.com>
2025-08-28 09:41:03 +00:00
swiitchOFF
187c0074e1 dolby: configs: Enable ac4
* Sony Dolby Atmos have working ac4 on Tidal/Hotstar
2025-08-28 09:41:03 +00:00
swiitchOFF
6b8b7bcae4 dolby: Update readme 2025-08-28 09:41:03 +00:00
swiitchOFF
e2df31f5b8 dolby: Enable ELF checks for libspatialaudio 2025-08-28 09:41:03 +00:00
New Author Name
10d3ac30f4 dolby: Link dolby blobs against v33 libstagefright_foundation
* fixes crashes and absurdly high CPU usage while using dolby

Co-authored-by: Woomymy <woomy@woomy.be>
2025-08-28 09:41:03 +00:00
swiitchOFF
0ea2536cf8 dolby: configs: Kang dax-default from https://github.com/reiryuki/Dolby-Atmos-Spatial-Sound-Moto-Edge-30-Ultra-Magisk-Module 2025-08-28 09:41:03 +00:00
swiitchOFF
8995c02d2d dolby: Kang daxService.apk and MotoDolbyDax3.apk
* From https://t.me/AMRGAMAL_STORE/5508
2025-08-28 09:41:02 +00:00
New Author Name
1c967d1859 dolby: permissions: Kang privapp-com.motorola.dolby.dolbyui.xml 2025-08-28 09:41:02 +00:00
swiitchOFF
82e51d2309 dolby: Switch to Motorola Dolby Interface
* kang from https://github.com/swiitch-OFF-Lab/hardware_dolby/tree/A14-moto
2025-08-28 09:41:02 +00:00
swiitchOFF
ee2f65b964 dolby: Enable elf checks
* Properly link dolby libs against v33 libstagefright_foundation
2025-03-26 06:33:10 +05:30
New Author Name
3c550064a0 dolby: configs: Kang dax-default from https://github.com/reiryuki/Dolby-Atmos-Sony-Xperia-5-V-Magisk-Module 2025-03-25 20:00:04 +05:30
swiitchOFF
56dc82a0a8 dolby: Switch to Sony Dolby Interface
* Taken from https://github.com/reiryuki/Dolby-Atmos-Sony-Xperia-5-V-Magisk-Module
2025-03-12 18:46:39 +05:30
swiitchOFF
496446c564 dolby: Kill XiaomiDolby 2025-03-12 18:44:55 +05:30
New Author Name
a6202e7f90 dolby: Switch to Sony Dolby Atmos
* Based on c2 audio decoders

Co-Authored-By: Saku <saku-bruh@proton.me>
Co-Authored-By: ahnet-69 <syedmusicrealise@gmail.com>
Co-Authored-By: HELLBOY017 <abhaygill017@gmail.com>
Signed-off-by: swiitchOFF <120115258+swiitchOFF@users.noreply.github.com>
2025-03-12 18:44:06 +05:30
New Author Name
00d74c76bc dolby: Apply NLSound 4.1
Ref:
https://github.com/Briclyaz/NLSound_module_QCom

Applied with slight modification:
sed -i 's/<mi-dv-leveler-steering-enable value="true"/<mi-dv-leveler-steering-enable value="false"/g' dax-default.xml
sed -i 's/<mi-ieq-steering-enable value="true"/<mi-ieq-steering-enable value="false"/g' dax-default.xml
sed -i 's/<mi-surround-compressor-steering-enable value="true"/<mi-surround-compressor-steering-enable value="false"/g' dax-default.xml
sed -i 's/<mi-adaptive-virtualizer-steering-enable value="true"/<mi-adaptive-virtualizer-steering-enable value="false"/g' dax-default.xml
sed -i 's/<reverb-suppression-enable value="true"/<reverb-suppression-enable value="false"/g' dax-default.xml
sed -i 's/<mi-dialog-enhancer-steering-enable value="true"/<mi-dialog-enhancer-steering-enable value="false"/g' dax-default.xml
sed -i 's/<dialog-enhancer-enable value="true"/<dialog-enhancer-enable value="false"/g' dax-default.xml
sed -i 's/<mi-virtualizer-binaural-steering-enable value="true"/<mi-virtualizer-binaural-steering-enable value="false"/g' dax-default.xml
sed -i 's/<peak-value value=".*"/<peak-value value="256"/g' dax-default.xml
sed -i 's/<surround-decoder-enable value="true"/<surround-decoder-enable value="false"/g' dax-default.xml
sed -i 's/<hearing-protection-enable value="true"/<hearing-protection-enable value="false"/g' dax-default.xml
sed -i 's/<volume-leveler-enable value="true"/<volume-leveler-enable value="false"/g' dax-default.xml
sed -i 's/<height-filter-mode value=".*"/<height-filter-mode value="0"/g' dax-default.xml
sed -i 's/<volume-leveler-compressor-enable value="true"/<volume-leveler-compressor-enable value="false"/g' dax-default.xml
sed -i 's/<complex-equalizer-enable value="true"/<complex-equalizer-enable value="false"/g' dax-default.xml
sed -i 's/<regulator-enable value="true"/<volume-leveler-enable value="false"/g' dax-default.xml
sed -i 's/<regulator-speaker-dist-enable value="true"/<regulator-speaker-dist-enable value="false"/g' dax-default.xml
sed -i 's/<regulator-sibilance-suppress-enable value="true"/<regulator-sibilance-suppress-enable value="false"/g' dax-default.xml
sed -i 's/bass-mbdrc-enable value="true"/bass-mbdrc-enable value="false"/g' dax-default.xml
sed -i 's/threshold_low=".*" threshold_high=".*"/threshold_low="0" threshold_high="0"/g' dax-default.xml
sed -i 's/isolated_band="true"/isolated_band="false"/g' dax-default.xml
sed -i '/endpoint_type="headphone"/,/<\/tuning>/s/<audio-optimizer-enable value="true"/<audio-optimizer-enable value="false"/g' dax-default.xml
sed -i '/<output-mode>/,/<\/output-mode>/d' dax-default.xml
sed -i '/<mix_matrix>/,/<\/output-mode>/d' dax-default.xml

Change-Id: I7d699d38d602abda322b414bdd718857a0bbaaa8
2025-01-29 03:49:45 +05:30
Alcatraz323
23662b74ac dolby: Implement DSPVolumeSynchronizer
Some Xiaomi devices have a speaker that needs a framework to cooperate
with DSP to synchronize volume so that the DSP can limit bass when the
volume is high to prevent distortion.

Change-Id: I750803d94161e1e7482552d2a39566f42e82fc0a
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
2025-01-23 04:50:45 +05:30
Pranav Vashi
65d7336949 dolby: Exempt installing package in clone or private space
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
2025-01-08 06:13:35 +05:30
New Author Name
0befa6aa8c dolby: Update profiles overlay for moto dolby
Change-Id: Ided92625cb73f530656e86929b3ba70fea1e940d
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
2024-12-27 19:30:37 +05:30
New Author Name
6d89341732 dolby: XiaomiDolby: Make bass enhancer available on speakers as well
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-12-26 15:22:45 +05:30
swiitchOFF
f5fb608d13 dolby: Feels better
[1]: cleanup and organize
[2]: using patched blobs from here 40faff25cd
2024-12-26 15:22:08 +05:30
swiitchOFF
6cc95d2a09 dolby: Update Credits 2024-12-09 22:12:54 +05:30
New Author Name
048d3bfbe3 dolby: Initialized Spatial Audio
Signed-off-by: userariii <abhattacharjee717@gmail.com>
Signed-off-by: swiitchOFF <120115258+swiitchOFF@users.noreply.github.com>
2024-12-06 12:32:59 +05:30
swiitchOFF
fac9089afd dolby: Sorry LDAC & LHDC
mistake beef2e28a1
2024-12-06 12:32:53 +05:30
swiitchOFF
724f607cb8 dolby: configs: Switch back to CN dax-default
[1]: Also Forcefully disable volume leveler
2024-12-03 05:39:47 +05:30
johnmart19
da758a7873 dolby: configs: Enable bass-enhancer for Headphones, Speakers, and "Other" devices
Original commit:
d6e99e9cd1

Change-Id: If32d0e1c2ddad5d8668c83bfbfcd04c36fa80552
Signed-off-by: someone5678 <nemui3353@gmail.com>
2024-11-25 19:20:34 +05:30
New Author Name
a54c599e2b dolby: configs: Forcefully disable volume leveler
sed -i "/volume-leveler-enable/ s/true/false/g" configs/audio/dax-default.xml

Change-Id: I7ceb29291a9e55036d57975f5a28a149c7fcb435
2024-11-25 17:31:31 +05:30
New Author Name
9c8b952778 dolby: Replace nuwa libs with Moto
* Also Kill nuwa Dolby spatial audio support
2024-11-25 17:31:09 +05:30
New Author Name
9bce0718c3 dolby: overlay: Add empty Dolby specific frameworks overlay 2024-11-24 13:52:15 +05:30
New Author Name
4401819065 dolby: Update readme 2024-11-22 01:05:46 +05:30
New Author Name
6fe60d867d dolby: Update from nuwa V14.0.9.0.UMBEUXM
* Also Import 32-bit blobs
2024-11-22 01:05:32 +05:30
Mudit200408
beef2e28a1 dolby: Add LDAC & LHDC codecs
* I know it's Placebo xD
2024-11-19 23:47:29 +05:30
Sarthak Roy
efb871f672 dolby: DolbyManager: Fix building in Android 15
Change-Id: Ie4e312bce0232c7a55ed2c29c8442f886f5aabd9
2024-11-09 19:24:26 +05:30
New Author Name
1f2bc3d255 dolby: Switch to XiaomiDolby
Based on hardware/xiaomi

History -
- Enable TARGET_USES_DOLBY
- Add intelligent equalizer setting
- Remove deprecated PlainTooltipBox
- Introduce graphical equalizer
- Add launcher icon
- fixup! Restore all settings upon bootup
- Override AudioFx
- Use all shared resources from devicesettings
- Fix build with kotlinc 1.9.0
- Restore current profile _after_ resetting profiles
- Do not set volume leveler amount
- Restore all settings upon bootup
- Rewrite in Kotlin
- Revert "Re-enable speaker virtualization after bootup"
- Convert to SwitchPreferenceCompat
- Migrate to CompoundButton.OnCheckedChangeListener
- Enable use_resource_processor for all sysui deps
- Introduce Dolby Atmos

Co-authored-by: Henrique Silva <jhenrique09.mcz@hotmail.com>
Co-authored-by: Pranav Vashi <neobuddy89@gmail.com>
Co-authored-by: Fabian Leutenegger <fabian.leutenegger@bluewin.ch>
Co-authored-by: basamaryan <basam.aryan@gmail.com>
2024-10-28 13:33:07 +05:30
Abhay Singh Gill
7870b8a0c7 dolby: Remove owner from soong modules
* also kill OnePlus DaxUI

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-10-28 13:32:36 +05:30
New Author Name
6ad0f598e9 dolby: Add dolby volume listener 2024-10-24 18:41:42 +05:30
New Author Name
174e52ec51 dolby: Add support for Dolby spatial audio 2024-10-24 18:41:34 +05:30
New Author Name
881bb90ebe dolby: Build some libs for QPR3 2024-10-24 13:01:00 +05:30
swiitchOFF
3fa57e3935 dolby: update from MIUI CN 14 2024-10-24 13:00:54 +05:30
swiitchOFF
07b0a125fd dolby: Update readme 2024-03-16 18:58:29 +05:30
New Author Name
fe709e060d dolby: update dax-default from stock
* Extract From vili-user 13 TKQ1.220829.002 V14.0.16.0.TKDEUXM release-keys
2024-03-16 18:48:10 +05:30
Lunarixus
e96e8c76fd dolby: Add Remove Packages for Dolby Environment 2024-03-16 09:38:34 +05:30
Abhay Singh Gill
31843fb4e7 dolby: DaxUI: Fix clipped dialogue enhancer seekbar thumb 2024-03-16 01:33:37 +05:30
New Author Name
f167291caf dolby: Link dolby blobs against v33 libstagefright_foundation
* fixes crashes and absurdly high CPU usage while using dolby

Co-authored-by: Woomymy <woomy@woomy.be>
2024-03-16 01:32:39 +05:30
swiitchOFF
773b6cba9e dolby: Import vili blobs
* Extract From vili-user 13 TKQ1.220829.002 V14.0.16.0.TKDEUXM release-keys
2024-03-16 01:23:06 +05:30
swiitchOFF
04bec17707 dolby: cleanup for Re-Base 2024-03-16 01:12:21 +05:30
swiitchOFF
a952ccb07b dolby: oneplus>xiaomi 2024-03-15 14:06:50 +05:30
Abhay Singh Gill
d2d27807cc dolby: Add path variable 2024-03-15 14:02:46 +05:30
Abhay Singh Gill
667b4721be dolby: Update readme 2024-03-15 14:02:41 +05:30
Abhay Singh Gill
258557eec9 dolby: DaxUI: Add more switches
For:-
• volume leveler
• surround virtualizer
• bass enhancer

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-03-15 14:02:31 +05:30
115 changed files with 10819 additions and 733 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.vscode/

View File

@@ -15,26 +15,299 @@
soong_namespace {
}
android_app_import {
name: "daxService",
owner: "oneplus",
apk: "proprietary/system_ext/priv-app/daxService/daxService.apk",
certificate: "platform",
dex_preopt: {
enabled: false,
cc_prebuilt_library_shared {
name: "libdapparamstorage",
owner: "xiaomi",
strip: {
none: true,
},
privileged: true,
system_ext_specific: true,
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libdapparamstorage.so"],
shared_libs: ["libcutils", "libutils", "liblog", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
android_app_import {
name: "DaxUI",
owner: "oneplus",
apk: "proprietary/system_ext/priv-app/DaxUI/DaxUI.apk",
certificate: "platform",
dex_preopt: {
enabled: false,
cc_prebuilt_library_shared {
name: "libdlbdsservice",
owner: "xiaomi",
strip: {
none: true,
},
privileged: true,
system_ext_specific: true,
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libdlbdsservice.so"],
shared_libs: ["libutils", "libcutils", "libstagefright_foundation-v33", "liblog", "libxml2", "libcrypto", "libdapparamstorage", "libsqlite", "libhidlbase", "vendor.dolby.hardware.dms@2.0", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libdlbpreg",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libdlbpreg.so"],
shared_libs: ["liblog", "libutils", "libcutils", "libaudioutils", "libstagefright_foundation-v33", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libspatializerparamstorage",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libspatializerparamstorage.so"],
shared_libs: ["libcutils", "libutils", "liblog", "libxml2", "libdapparamstorage", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "vendor.dolby.hardware.dms@2.0-impl",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/vendor.dolby.hardware.dms@2.0-impl.so"],
shared_libs: ["libhidlbase", "libutils", "liblog", "libdapparamstorage", "libdlbdsservice", "vendor.dolby.hardware.dms@2.0", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "vendor.dolby.hardware.dms@2.0",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/vendor.dolby.hardware.dms@2.0.so"],
shared_libs: ["libhidlbase", "liblog", "libutils", "libcutils", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_binary {
name: "vendor.dolby.hardware.dms@2.0-service",
owner: "xiaomi",
target: {
android_arm64: {
srcs: ["proprietary/vendor/bin/hw/vendor.dolby.hardware.dms@2.0-service"],
shared_libs: ["liblog", "libutils", "libhidlbase", "libdapparamstorage", "libdlbdsservice", "vendor.dolby.hardware.dms@2.0", "vendor.dolby.hardware.dms@2.0-impl", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
strip: {
none: true,
},
prefer: true,
relative_install_path: "hw",
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libcodec2_soft_ac4dec",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libcodec2_soft_ac4dec.so"],
shared_libs: ["libhidlbase", "vendor.dolby.hardware.dms@2.0", "libdeccfg", "libbase", "libcodec2", "libcodec2_vndk", "libutils", "libcodec2_soft_common", "libcutils", "liblog", "libsfplugin_ccodec_utils", "libstagefright_foundation-v33", "libcodec2_store_dolby", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libcodec2_soft_ddpdec",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libcodec2_soft_ddpdec.so"],
shared_libs: ["libhidlbase", "vendor.dolby.hardware.dms@2.0", "libdeccfg", "libbase", "libcodec2", "libcodec2_vndk", "libutils", "libcodec2_soft_common", "libcutils", "liblog", "libsfplugin_ccodec_utils", "libstagefright_foundation-v33", "libcodec2_store_dolby", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libcodec2_store_dolby",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libcodec2_store_dolby.so"],
shared_libs: ["libdmabufheap", "libbase", "liblog", "libcodec2", "libcodec2_vndk", "libutils", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libdeccfg",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/libdeccfg.so"],
shared_libs: ["libhidlbase", "vendor.dolby.hardware.dms@2.0", "libdapparamstorage", "libcutils", "libutils", "liblog", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
prefer: true,
soc_specific: true,
}
cc_prebuilt_binary {
name: "vendor.dolby.media.c2@1.0-service",
owner: "xiaomi",
target: {
android_arm64: {
srcs: ["proprietary/vendor/bin/hw/vendor.dolby.media.c2@1.0-service"],
shared_libs: ["libbase", "liblog", "libcodec2", "libutils", "android.hardware.media.c2@1.0", "android.hardware.media.c2@1.1", "android.hardware.media.c2@1.2", "libcodec2_hidl@1.0", "libcodec2_hidl@1.1", "libcodec2_hidl@1.2", "libcodec2_vndk", "libhidlbase", "libavservices_minijail", "libbinder", "libcodec2_store_dolby", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
strip: {
none: true,
},
prefer: true,
relative_install_path: "hw",
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libdlbvol",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/soundfx/libdlbvol.so"],
shared_libs: ["liblog", "libutils", "libcutils", "libaudioutils", "libstagefright_foundation-v33", "libdlbpreg", "vendor.dolby.hardware.dms@2.0", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
relative_install_path: "soundfx",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libhwdap",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/soundfx/libhwdap.so"],
shared_libs: ["libspatializerparamstorage", "liblog", "libutils", "libcutils", "libaudioutils", "libdapparamstorage", "libhidlbase", "libdlbpreg", "vendor.dolby.hardware.dms@2.0", "libstagefright_foundation-v33", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
relative_install_path: "soundfx",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libswgamedap",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/soundfx/libswgamedap.so"],
shared_libs: ["libspatializerparamstorage", "liblog", "libutils", "libcutils", "libaudioutils", "libdapparamstorage", "libhidlbase", "vendor.dolby.hardware.dms@2.0", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
relative_install_path: "soundfx",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libswspatializer",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/soundfx/libswspatializer.so"],
shared_libs: ["liblog", "libutils", "libcutils", "libaudioutils", "libdapparamstorage", "libspatializerparamstorage", "libhidlbase", "libstagefright_foundation-v33", "libdlbpreg", "vendor.dolby.hardware.dms@2.0", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
relative_install_path: "soundfx",
prefer: true,
soc_specific: true,
}
cc_prebuilt_library_shared {
name: "libswvqe",
owner: "xiaomi",
strip: {
none: true,
},
target: {
android_arm64: {
srcs: ["proprietary/vendor/lib64/soundfx/libswvqe.so"],
shared_libs: ["libspatializerparamstorage", "liblog", "libutils", "libcutils", "libaudioutils", "libdapparamstorage", "libhidlbase", "vendor.dolby.hardware.dms@2.0", "libc++", "libc", "libm", "libdl", ],
},
},
compile_multilib: "64",
relative_install_path: "soundfx",
prefer: true,
soc_specific: true,
}

View File

@@ -1,9 +1,9 @@
OnePlus Dolby
AOSPA Dolby
==============
Getting Started
---------------
Make sure you are not using any audio effect configuration in your device trees. Also for dolby media codecs to work add this line in your media codecs config (should be in vendor partition) :-
For dolby media codecs to work add this line in your media codecs config (should be in vendor partition) :-
```bash
<Include href="media_codecs_dolby_audio.xml" />
@@ -15,3 +15,47 @@ To build, add the dolby effects in your device's audio effects config then inher
$(call inherit-product, hardware/dolby/dolby.mk)
```
Now, moving hidl definitions in manifest to device trees is completely absurd so stop overriding manifest in your device trees an example for such would be :-
Changing these in BoardConfig makefile of your device tree:-
```bash
DEVICE_FRAMEWORK_COMPATIBILITY_MATRIX_FILE :=
```
And
```bash
DEVICE_MANIFEST_FILE :=
```
To:-
```bash
DEVICE_FRAMEWORK_COMPATIBILITY_MATRIX_FILE +=
```
And
```bash
DEVICE_MANIFEST_FILE +=
```
The only change done above is changing := symbol to += so that manifest can't be overriden from device tree in BoardConfig makefile.
At the end an example commit to properly implement it in your device tree could be :-
[**1**] https://github.com/Spanish-or-Vanish/android_device_xiaomi_sm8350-common/commit/8ece9a976087ed03f1adb447d0ac2a3f5f1a0d3c
[**2**] https://github.com/Spanish-or-Vanish/android_device_xiaomi_sm8350-common/commit/2bdd047bfb5f23bd02e6d4db601a97c27e1506f9
#### Tip pick anyone for Reference:
https://dumps.tadiphone.dev/dumps/xiaomi/vili/-/blob/missi_phone_global-user-14-UKQ1.231207.002-V816.0.5.0.UKDMIXM-release-keys/aosp-device-tree/proprietary-files.txt#L2072
https://github.com/Spanish-or-Vanish/android_device_xiaomi_sm8350-common/commit/6a8f6aae347f9f864f17f2dfa5d541f83ab5b170
https://github.com/Spanish-or-Vanish/android_device_xiaomi_sm8350-common/commit/5c593cbd2c19d571b7e62ea4bd6b8c864a4e1d9e
# Credits:
* [**HELLBOY017**](https://github.com/HELLBOY017)
* [**adithya2306**](https://github.com/adithya2306)
* [**johnmart19**](https://github.com/johnmart19)
* [**userariii**](https://github.com/userariii)
* [**saku-bruh**](https://github.com/saku-bruh)
* [**ahnet-69 · he/him**](https://github.com/ahnet-69)

View File

@@ -0,0 +1,10 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := RemovePackagesDolby
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_OVERRIDES_PACKAGES += Music MusicFX AudioFX
LOCAL_UNINSTALLABLE_MODULE := true
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_SRC_FILES := /dev/null
include $(BUILD_PREBUILT)

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Feature for devices with a head tracker sensor. -->
<permissions>
<feature name="android.hardware.sensor.dynamic.head_tracker" />
</permissions>

1480
configs/dax-default.xml Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2016 The Android Open Source Project
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This file was modified by Dolby Laboratories, Inc. The portions of the
code that are surrounded by "DOLBY..." are copyrighted and
licensed separately, as follows:
(C) 2018 Dolby Laboratories, Inc.
All rights reserved.
This program is protected under international and U.S. Copyright laws as
This program is protected under international and U.S. copyright laws as
an unpublished work. This program is confidential and proprietary to the
copyright owners. Reproduction or disclosure, in whole or in part, or the
production of derivative works therefrom without the express permission of
the copyright owners is prohibited.
Copyright (C) 2020-2021 by Dolby Laboratories,
All rights reserved.
-->
<Included>
<Decoders>
<!-- DOLBY_UDC -->
<MediaCodec name="OMX.dolby.ac3.decoder" type="audio/ac3">
<Limit name="channel-count" max="6" />
<Limit name="sample-rate" ranges="32000,44100,48000" />
<Limit name="bitrate" range="32000-640000" />
</MediaCodec>
<MediaCodec name="OMX.dolby.eac3.decoder" type="audio/eac3">
<Limit name="channel-count" max="8" />
<Limit name="sample-rate" ranges="32000,44100,48000" />
<Limit name="bitrate" range="32000-6144000" />
</MediaCodec>
<MediaCodec name="OMX.dolby.eac3-joc.decoder" type="audio/eac3-joc">
<Limit name="channel-count" max="8" />
<Limit name="sample-rate" ranges="48000" />
<Limit name="bitrate" range="32000-6144000" />
<MediaCodec name="c2.dolby.eac3.decoder" >
<Type name="audio/ac3">
<Alias name="OMX.dolby.ac3.decoder" />
<Limit name="channel-count" max="6" />
<Limit name="sample-rate" ranges="32000,44100,48000" />
<Limit name="bitrate" range="32000-640000" />
</Type>
<Type name="audio/eac3">
<Alias name="OMX.dolby.eac3.decoder" />
<Limit name="channel-count" max="8" />
<Limit name="sample-rate" ranges="32000,44100,48000" />
<Limit name="bitrate" range="32000-6144000" />
</Type>
<Type name="audio/eac3-joc">
<Alias name="OMX.dolby.eac3-joc.decoder" />
<Limit name="channel-count" max="16" />
<Limit name="sample-rate" ranges="48000" />
<Limit name="bitrate" range="32000-6144000" />
</Type>
<Attribute name="software-codec" />
</MediaCodec>
<!-- DOLBY_UDC END -->
<!-- DOLBY_AC4 -->
<!-- <MediaCodec name="c2.dolby.ac4.decoder" type="audio/ac4">
<Alias name="OMX.dolby.ac4.decoder" />
<Limit name="channel-count" max="16" />
<Limit name="sample-rate" ranges="48000" />
<Limit name="bitrate" range="16000-2688000" />
</MediaCodec> -->
<!-- DOLBY_AC4 END -->
</Decoders>
</Included>

153
dolby.mk
View File

@@ -14,67 +14,118 @@
# limitations under the License.
#
# Dolby path
DOLBY_PATH := hardware/dolby
# Soong Namespace
PRODUCT_SOONG_NAMESPACES += \
hardware/dolby
$(DOLBY_PATH)
# Enable codec support
AUDIO_FEATURE_ENABLED_DS2_DOLBY_DAP := true
# SEPolicy
BOARD_VENDOR_SEPOLICY_DIRS += hardware/dolby/sepolicy/vendor
# HIDL
DEVICE_FRAMEWORK_COMPATIBILITY_MATRIX_FILE += hardware/dolby/dolby_framework_matrix.xml
DEVICE_MANIFEST_FILE += hardware/dolby/vendor.dolby.hardware.dms@2.0-service.xml
BOARD_VENDOR_SEPOLICY_DIRS += $(DOLBY_PATH)/sepolicy/vendor
# Configs
PRODUCT_COPY_FILES += \
hardware/dolby/configs/dax-default.xml:$(TARGET_COPY_OUT_VENDOR)/etc/dolby/dax-default.xml \
hardware/dolby/configs/media_codecs_dolby_audio.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_codecs_dolby_audio.xml
$(DOLBY_PATH)/configs/dax-default.xml:$(TARGET_COPY_OUT_VENDOR)/etc/dolby/dax-default.xml \
$(DOLBY_PATH)/configs/media_codecs_dolby_audio.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_codecs_dolby_audio.xml
# Dolby
PRODUCT_VENDOR_PROPERTIES += \
ro.vendor.dolby.dax.version=DAX3_3.6.0.12_r1 \
ro.vendor.product.device.db=OP_DEVICE \
ro.vendor.product.manufacturer.db=OP_PHONE \
vendor.product.device=OP_PHONE \
vendor.product.manufacturer=OPD
# DaxUI and daxService
# Dolby VNDK libs
PRODUCT_PACKAGES += \
DaxUI \
daxService
# Proprietary blobs
libstagefright_foundation-v33
PRODUCT_PACKAGES += \
libshim_dolby
# Init
PRODUCT_PACKAGES += \
init.dolby.rc
# Overlays
PRODUCT_PACKAGES += \
DolbyFrameworksResCommon
# Dolby Spatial Audio
PRODUCT_COPY_FILES += \
hardware/dolby/proprietary/system/lib64/vendor.dolby.hardware.dms@2.0.so:$(TARGET_COPY_OUT_SYSTEM)/lib64/vendor.dolby.hardware.dms@2.0.so \
hardware/dolby/proprietary/system_ext/etc/sysconfig/config-com.dolby.daxappui.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/sysconfig/config-com.dolby.daxappui.xml \
hardware/dolby/proprietary/system_ext/etc/sysconfig/config-com.dolby.daxservice.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/sysconfig/config-com.dolby.daxservice.xml \
hardware/dolby/proprietary/system_ext/etc/sysconfig/hiddenapi-com.dolby.daxservice.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/sysconfig/hiddenapi-com.dolby.daxservice.xml \
hardware/dolby/proprietary/system_ext/etc/permissions/privapp-com.dolby.daxappui.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/permissions/privapp-com.dolby.daxappui.xml \
hardware/dolby/proprietary/system_ext/etc/permissions/privapp-com.dolby.daxservice.xml:$(TARGET_COPY_OUT_SYSTEM_EXT)/etc/permissions/privapp-com.dolby.daxservice.xml \
hardware/dolby/proprietary/vendor/bin/hw/vendor.dolby.hardware.dms@2.0-service:$(TARGET_COPY_OUT_VENDOR)/bin/hw/vendor.dolby.hardware.dms@2.0-service \
hardware/dolby/proprietary/vendor/etc/init/vendor.dolby.hardware.dms@2.0-service.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/vendor.dolby.hardware.dms@2.0-service.rc \
hardware/dolby/proprietary/vendor/lib/libdapparamstorage.so:$(TARGET_COPY_OUT_VENDOR)/lib/libdapparamstorage.so \
hardware/dolby/proprietary/vendor/lib/libdeccfg.so:$(TARGET_COPY_OUT_VENDOR)/lib/libdeccfg.so \
hardware/dolby/proprietary/vendor/lib/libqtigef.so:$(TARGET_COPY_OUT_VENDOR)/lib/libqtigef.so \
hardware/dolby/proprietary/vendor/lib/libstagefright_soft_ddpdec.so:$(TARGET_COPY_OUT_VENDOR)/lib/libstagefright_soft_ddpdec.so \
hardware/dolby/proprietary/vendor/lib/libstagefrightdolby.so:$(TARGET_COPY_OUT_VENDOR)/lib/libstagefrightdolby.so \
hardware/dolby/proprietary/vendor/lib/soundfx/libeffectproxy.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libeffectproxy.so \
hardware/dolby/proprietary/vendor/lib/soundfx/libhwdap.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libhwdap.so \
hardware/dolby/proprietary/vendor/lib/soundfx/libswdap.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libswdap.so \
hardware/dolby/proprietary/vendor/lib/soundfx/libswvqe.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libswvqe.so \
hardware/dolby/proprietary/vendor/lib/vendor.dolby.hardware.dms@2.0.so:$(TARGET_COPY_OUT_VENDOR)/lib/vendor.dolby.hardware.dms@2.0.so \
hardware/dolby/proprietary/vendor/lib64/libdapparamstorage.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdapparamstorage.so \
hardware/dolby/proprietary/vendor/lib64/libdeccfg.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdeccfg.so \
hardware/dolby/proprietary/vendor/lib64/libqtigef.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libqtigef.so \
hardware/dolby/proprietary/vendor/lib64/libdlbdsservice.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdlbdsservice.so \
hardware/dolby/proprietary/vendor/lib64/libstagefright_soft_ddpdec.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libstagefright_soft_ddpdec.so \
hardware/dolby/proprietary/vendor/lib64/libstagefrightdolby.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libstagefrightdolby.so \
hardware/dolby/proprietary/vendor/lib64/soundfx/libeffectproxy.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libeffectproxy.so \
hardware/dolby/proprietary/vendor/lib64/soundfx/libhwdap.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libhwdap.so \
hardware/dolby/proprietary/vendor/lib64/soundfx/libswdap.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libswdap.so \
hardware/dolby/proprietary/vendor/lib64/soundfx/libswvqe.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libswvqe.so \
hardware/dolby/proprietary/vendor/lib64/vendor.dolby.hardware.dms@2.0-impl.so:$(TARGET_COPY_OUT_VENDOR)/lib64/vendor.dolby.hardware.dms@2.0-impl.so \
hardware/dolby/proprietary/vendor/lib64/vendor.dolby.hardware.dms@2.0.so:$(TARGET_COPY_OUT_VENDOR)/lib64/vendor.dolby.hardware.dms@2.0.so
$(DOLBY_PATH)/configs/android.hardware.sensor.dynamic.head_tracker.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.dynamic.head_tracker.xml \
# Dolby Spatial Audio: optimize spatializer effect
PRODUCT_PROPERTY_OVERRIDES += \
audio.spatializer.effect.util_clamp_min=300
# Dolby Spatial Audio: declare use of spatial audio
PRODUCT_PROPERTY_OVERRIDES += \
ro.audio.spatializer_enabled=true \
ro.audio.headtracking_enabled=true \
ro.audio.spatializer_transaural_enabled_default=false \
persist.vendor.audio.spatializer.speaker_enabled=true \
# Dolby Spatial Audio Proprietary blobs
PRODUCT_PACKAGES += \
libspatializerparamstorage \
libswspatializer
# Media (C2)
PRODUCT_PACKAGES += \
android.hardware.media.c2@1.0.vendor \
android.hardware.media.c2@1.1.vendor \
android.hardware.media.c2@1.2.vendor \
libcodec2_hidl@1.2.vendor \
libsfplugin_ccodec_utils.vendor \
libcodec2_soft_common.vendor
# Codec2 Props
PRODUCT_VENDOR_PROPERTIES += \
vendor.audio.c2.preferred=true \
debug.c2.use_dmabufheaps=1 \
vendor.qc2audio.suspend.enabled=true \
vendor.qc2audio.per_frame.flac.dec.enabled=true
# Dolby Props
PRODUCT_VENDOR_PROPERTIES += \
ro.vendor.dolby.dax.version=DAX3_3.7.0.8_r1 \
vendor.audio.dolby.ds2.hardbypass=false \
vendor.audio.dolby.ds2.enabled=false
# Remove Packages for Dolby Support
PRODUCT_PACKAGES += \
RemovePackagesDolby
# XiaomiDolby
PRODUCT_PACKAGES += \
XiaomiDolby
# Dolby Proprietary blobs
PRODUCT_COPY_FILES += \
$(DOLBY_PATH)/proprietary/vendor/etc/init/vendor.dolby.hardware.dms@2.0-service.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/vendor.dolby.hardware.dms@2.0-service.rc
PRODUCT_PACKAGES += \
libdapparamstorage \
libdlbdsservice \
libdlbpreg \
vendor.dolby.hardware.dms@2.0-impl \
vendor.dolby.hardware.dms@2.0 \
vendor.dolby.hardware.dms@2.0-service
# Codec2 (Dolby)
PRODUCT_COPY_FILES += \
$(DOLBY_PATH)/proprietary/vendor/etc/init/vendor.dolby.media.c2@1.0-service.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/vendor.dolby.media.c2@1.0-service.rc
PRODUCT_PACKAGES += \
libcodec2_soft_ac4dec \
libcodec2_soft_ddpdec \
libcodec2_store_dolby \
libdeccfg \
vendor.dolby.media.c2@1.0-service
# Dolby SoundFX Blobs
PRODUCT_PACKAGES += \
libdlbvol \
libhwdap \
libswgamedap \
libswvqe

45
dolby/Android.bp Normal file
View File

@@ -0,0 +1,45 @@
//
// Copyright (C) 2017-2021 The LineageOS Project
// (C) 2023-24 Paranoid Android
// (C) 2024-2025 Lunaris AOSP
//
// SPDX-License-Identifier: Apache-2.0
//
android_app {
name: "XiaomiDolby",
srcs: ["src/**/*.kt"],
resource_dirs: ["res"],
certificate: "platform",
platform_apis: true,
system_ext_specific: true,
privileged: true,
overrides: ["MusicFX", "AudioFX"],
static_libs: [
"androidx.activity_activity-compose",
"androidx.compose.animation_animation",
"androidx.compose.foundation_foundation",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
"androidx.compose.ui_ui-tooling-preview",
"androidx.lifecycle_lifecycle-runtime-compose",
"androidx.lifecycle_lifecycle-viewmodel-compose",
"androidx.navigation_navigation-compose",
"com.google.android.material_material",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
],
required: ["preinstalled-packages-platform-dolby.xml"],
}
prebuilt_etc {
name: "preinstalled-packages-platform-dolby.xml",
src: "preinstalled-packages-platform-dolby.xml",
sub_dir: "sysconfig",
system_ext_specific: true,
}

101
dolby/AndroidManifest.xml Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023-24 Paranoid Android
Copyright (C) 2024-2025 Lunaris AOSP
SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.lunaris.dolby"
android:sharedUserId="android.uid.system">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
<application
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:label="@string/dolby_title"
android:enableOnBackInvokedCallback="true"
android:persistent="true"
android:directBootAware="true"
android:defaultToDeviceProtectedStorage="true"
android:theme="@style/Theme.Dolby">
<receiver
android:name=".BootCompletedReceiver"
android:exported="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".ui.DolbyActivity"
android:label="@string/dolby_title"
android:exported="true">
<intent-filter>
<action android:name="com.android.settings.action.IA_SETTINGS" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
</intent-filter>
<intent-filter>
<action android:name="android.media.action.DISPLAY_AUDIO_EFFECT_CONTROL_PANEL" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.CATEGORY_CONTENT_MUSIC" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.sound" />
<meta-data android:name="com.android.settings.summary_uri"
android:value="content://org.lunaris.dolby.summary/dolby" />
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.lunaris.dolby.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".tile.DolbyTileService"
android:icon="@drawable/ic_dolby_qs"
android:label="@string/dolby_title"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
</service>
<provider
android:name=".provider.SummaryProvider"
android:authorities="org.lunaris.dolby.summary">
</provider>
<service
android:name=".service.AppProfileMonitorService"
android:exported="false"
android:enabled="true" />
<service
android:name=".service.DolbyNotificationListener"
android:label="@string/dolby_notification_listener_label"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 crDroid Android Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<config>
<install-in-user-type package="org.lunaris.dolby">
<install-in user-type="FULL" />
<install-in user-type="PROFILE" />
<do-not-install-in user-type="android.os.usertype.profile.CLONE" />
<do-not-install-in user-type="android.os.usertype.profile.PRIVATE" />
</install-in-user-type>
</config>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#191c1e" android:pathData="M1,4.0214C2.2767,4.0743 3.5798,3.9866 4.8252,4.2063C8.8352,4.9133 11.4129,8.3489 11.0507,12.3402C10.7124,16.0695 7.3661,18.9511 3.3484,18.9651C2.5657,18.9678 1.7827,18.9441 1,18.9324L1,4.0214Z" android:strokeColor="#00000000" android:strokeWidth="1" android:fillType="evenOdd"/>
<group>
<clip-path android:pathData="M12.9332,4l10.0668,0l0,15l-10.0668,0z"/>
<path android:fillColor="#191c1e" android:pathData="M23,4.0924L23,18.8825C19.4973,19.298 16.399,18.6968 14.3366,15.6947C12.5148,13.043 12.4594,10.2265 14.2129,7.5241C16.244,4.394 19.3953,3.7204 23,4.0924" android:strokeColor="#00000000" android:strokeWidth="1" android:fillType="evenOdd"/>
</group>
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector android:height="108.0dip" android:width="108.0dip" android:viewportWidth="108.0" android:viewportHeight="108.0"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
<group android:scaleX="1.3544992" android:scaleY="1.3544992" android:translateX="-23.22" android:translateY="-23.22">
<group>
<clip-path android:pathData="M57.01,57.01m-54,0a54,54 0,1 1,108 0a54,54 0,1 1,-108 0" />
<path android:fillColor="#ffeaedef" android:pathData="M-1.9,-1.9h117.82v117.82h-117.82z" />
<path android:fillColor="@drawable/ic_launcher_background__0" android:pathData="M-1.9,115.92l0,-117.82l117.82,0l-117.82,117.82z" />
<path android:fillColor="@drawable/ic_launcher_background__1" android:pathData="M-1.9,-1.9h117.82v117.82h-117.82z" android:strokeAlpha="0.2" android:fillAlpha="0.2" />
</group>
</group>
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<gradient android:angle="0.0" android:type="linear" android:startX="57.01" android:startY="56.4" android:endX="57.01" android:endY="-1.58"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
<item android:color="#ffffffff" android:offset="0.0" />
<item android:color="#fff3f5f6" android:offset="1.0" />
</gradient>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<gradient android:angle="0.0" android:type="linear" android:startX="-1.9" android:startY="115.92" android:endX="115.92" android:endY="-1.9"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
<item android:color="#ff80d0ce" android:offset="0.0" />
<item android:color="#ff9fa8da" android:offset="1.0" />
</gradient>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector android:height="108.0dip" android:width="108.0dip" android:viewportWidth="108.0" android:viewportHeight="108.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<group android:scaleX="0.84" android:scaleY="0.84" android:translateX="23.76" android:translateY="23.76">
<path android:fillColor="#ff465461" android:pathData="M12,19.13h5a16.87,16.87 0,0 1,0 33.74H12Z" />
<path android:fillColor="#ff465461" android:pathData="M60,52.87H55a16.87,16.87 0,0 1,0 -33.74h5Z" />
</group>
</vector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="108dp" android:width="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:fillColor="#000000" android:pathData="M36.6305484,38 C45.6078241,38.1592687 52.837468,45.7082857 52.837468,54.9974325 C52.837468,64.2865794 45.6078241,71.8355964 36.6307694,71.9948651 L32,72 L32,38 L36.6305484,38 Z M36.999,43.017 L36.999,66.977 L37.1225728,66.9701894 C42.9872179,66.559014 47.6837958,61.5395304 47.8337726,55.3050128 L47.837468,54.9974325 C47.837468,48.6211219 43.0833381,43.4425908 37.1223719,43.0246757 L36.999,43.017 Z M71.3694516,38 C62.3921759,38.1592687 55.162532,45.7082857 55.162532,54.9974325 C55.162532,64.2865794 62.3921759,71.8355964 71.3692306,71.9948651 L76,72 L76,38 L71.3694516,38 Z M71.000532,43.017 L71.000532,66.977 L70.8774272,66.9701894 C65.0127821,66.559014 60.3162042,61.5395304 60.1662274,55.3050128 L60.162532,54.9974325 C60.162532,48.6211219 64.9166619,43.4425908 70.8776281,43.0246757 L71.000532,43.017 Z" android:strokeWidth="1"/>
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_mono" />
</adaptive-icon>

103
dolby/res/values/arrays.xml Normal file
View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023-24 Paranoid Android
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<!-- Dolby Atmos -->
<string-array name="dolby_profile_entries">
<item>@string/dolby_profile_dynamic</item>
<item>@string/dolby_profile_video</item>
<item>@string/dolby_profile_music</item>
<item>@string/dolby_profile_game</item>
<item>@string/dolby_profile_work</item>
<item>@string/dolby_profile_casual</item>
<item>@string/dolby_profile_mood</item>
</string-array>
<string-array name="dolby_profile_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
</string-array>
<string-array name="dolby_preset_entries" translatable="false">
<item>@string/dolby_preset_default</item>
<item>@string/dolby_preset_rock</item>
<item>@string/dolby_preset_jazz</item>
<item>@string/dolby_preset_pop</item>
<item>@string/dolby_preset_classical</item>
<item>@string/dolby_preset_hiphop</item>
<item>@string/dolby_preset_blues</item>
<item>@string/dolby_preset_electronic</item>
<item>@string/dolby_preset_metal</item>
<item>@string/dolby_preset_acoustic</item>
<item>@string/dolby_preset_bass</item>
<item>@string/dolby_preset_beats</item>
<item>@string/dolby_preset_clear</item>
<item>@string/dolby_preset_deep</item>
<item>@string/dolby_preset_dubstep</item>
<item>@string/dolby_preset_hardstyle</item>
<item>@string/dolby_preset_movie</item>
<item>@string/dolby_preset_rb</item>
<item>@string/dolby_preset_vocal</item>
</string-array>
<string-array name="dolby_preset_values">
<!--
Enhanced preset values with improved ±15dB range for better sound quality
Values are now scaled to ±150 range (±15dB) for more precise audio tuning
-->
<item>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</item>
<item>90,54,18,-18,-54,-36,-18,-12,-6,-30,-54,-30,-6,-30,-54,-24,6,48,90,90</item>
<item>12,12,12,12,12,0,-12,-12,-12,-36,-60,-30,0,6,12,12,12,12,12,12</item>
<item>-20,-2,17,-38,-92,-44,5,17,29,29,29,23,17,-14,-44,-14,17,23,29,29</item>
<item>-48,-48,-48,-48,-48,-48,-48,-42,-36,-6,24,0,-24,36,96,48,0,48,96,96</item>
<item>78,42,6,-30,-66,-36,-6,-6,-6,-36,-66,-36,-6,0,6,6,6,30,54,54</item>
<item>42,42,42,-54,-150,-102,-54,6,66,42,18,6,-6,6,18,6,-6,18,42,42</item>
<item>75,51,27,3,-21,-9,3,-3,-9,-39,-69,-39,-9,-3,3,3,3,3,3,3</item>
<item>60,36,12,12,12,-6,-24,-18,-12,-48,-84,-36,12,12,12,12,12,12,12,12</item>
<item>50,45,40,35,15,10,15,15,20,30,35,40,37,30,30,30,25,20,15,15</item>
<item>100,88,85,65,25,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0</item>
<item>-55,-50,-45,-42,-35,-30,-19,0,0,0,0,0,0,0,0,0,0,0,0,0</item>
<item>35,55,65,95,80,65,35,25,13,50,70,90,100,110,90,85,75,65,55,45</item>
<item>120,80,0,-67,-120,-90,-35,-35,-61,0,-30,-50,0,12,30,25,20,15,10,5</item>
<item>120,100,5,-10,-30,-50,-50,-48,-45,-25,-10,0,-25,-25,0,0,0,0,0,0</item>
<item>61,70,120,61,-50,-120,-25,30,65,0,-22,-45,-61,-92,-100,-95,-90,-85,-80,-75</item>
<item>30,30,61,85,90,70,61,61,50,80,35,35,80,100,80,75,70,65,60,55</item>
<item>30,30,70,61,45,15,-15,-20,-15,20,25,30,35,38,40,38,35,32,30,28</item>
<item>-15,-20,-30,-30,-5,15,35,35,35,30,20,15,0,0,-15,-12,-10,-8,-6,-4</item>
</string-array>
<string-array name="dolby_ieq_entries">
<item>@string/dolby_off</item>
<item>@string/dolby_detailed</item>
<item>@string/dolby_balanced</item>
<item>@string/dolby_warm</item>
</string-array>
<string-array name="dolby_ieq_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
<string-array name="dolby_bass_curve_entries">
<item>@string/dolby_bass_curve_balanced</item>
<item>@string/dolby_bass_curve_sub_boost</item>
<item>@string/dolby_bass_curve_punch</item>
</string-array>
<string-array name="dolby_bass_curve_values">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">@android:color/system_accent1_400</color>
</resources>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023-25 Paranoid Android
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<!-- Whether stereo widening levels are supported -->
<bool name="dolby_stereo_widening_supported">true</bool>
<!-- Whether volume leveler is supported -->
<bool name="dolby_volume_leveler_supported">true</bool>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2025 Paranoid Android
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<!-- Stereo widening amount -->
<integer name="stereo_widening_min">4</integer>
<integer name="stereo_widening_max">64</integer>
<!-- Dialogue enhancer amount -->
<integer name="dialogue_enhancer_min">1</integer>
<integer name="dialogue_enhancer_max">12</integer>
</resources>

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023-25 Paranoid Android
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<!-- Dolby Atmos -->
<string name="dolby_title">Dolby Atmos</string>
<string name="dolby_enable">Use Dolby Atmos</string>
<string name="dolby_profile_title">Choose a profile</string>
<string name="dolby_preset">Equalizer</string>
<string name="dolby_off">Off</string>
<string name="dolby_on">On</string>
<string name="dolby_low">Low</string>
<string name="dolby_medium">Medium</string>
<string name="dolby_high">High</string>
<string name="dolby_max">Max</string>
<string name="dolby_unknown">Unknown</string>
<string name="dolby_on_with_profile">On (%1$s)</string>
<string name="dolby_category_settings">Audio tuning</string>
<string name="dolby_category_adv_settings">Advanced settings</string>
<string name="dolby_adv_settings_footer">Choose a different profile to show advanced settings.</string>
<string name="dolby_bass_enhancer">Bass enhancer</string>
<string name="dolby_bass_level">Bass level</string>
<string name="dolby_bass_curve">Bass curve</string>
<string name="dolby_bass_curve_balanced">Balanced</string>
<string name="dolby_bass_curve_sub_boost">Sub boost</string>
<string name="dolby_bass_curve_punch">Punch</string>
<string name="dolby_treble_enhancer">Treble enhancer</string>
<string name="dolby_treble_level">Treble level</string>
<string name="dolby_dialogue_enhancer">Dialogue enhancer</string>
<string name="dolby_spk_virtualizer">Speaker surround virtualizer</string>
<string name="dolby_hp_virtualizer">Headphone surround virtualizer</string>
<string name="dolby_volume_leveler">Volume leveler</string>
<string name="dolby_bass_enhancer_summary">Enhances low-frequency sounds for deeper bass</string>
<string name="dolby_treble_enhancer_summary">Enhances high-frequency sounds for clearer treble</string>
<string name="dolby_dialogue_enhancer_summary">Improves clarity of voices and speech</string>
<string name="dolby_spk_virtualizer_summary">Creates virtual surround sound on speakers</string>
<string name="dolby_hp_virtualizer_summary">Creates virtual surround sound on headphones</string>
<string name="dolby_volume_leveler_summary">Balances volume for a consistent listening experience</string>
<string name="dolby_connect_headphones">Connect headphones</string>
<string name="dolby_reset_all">Reset profiles</string>
<string name="dolby_reset_all_message">This will reset all profiles to factory defaults.</string>
<string name="dolby_reset_all_toast">Succesfully reset all profiles</string>
<string name="dolby_hp_virtualizer_dolby_strength">Surround virtualizer Strength</string>
<string name="dolby_dialogue_enhancer_dolby_strength">Dialogue enhancer Strength</string>
<string name="dolby_notification_listener_label">Dolby Atmos Service</string>
<string name="loading">Loading...</string>
<!-- Dolby profiles -->
<string name="dolby_profile_dynamic">Dynamic</string>
<string name="dolby_profile_video">Movie/Video</string>
<string name="dolby_profile_music">Music</string>
<string name="dolby_profile_game">Game</string>
<string name="dolby_profile_work">Work</string>
<string name="dolby_profile_casual">Casual</string>
<string name="dolby_profile_mood">Mood</string>
<!-- Dolby equalizer presets -->
<string name="dolby_preset_default">Flat (off)</string>
<string name="dolby_preset_rock">Rock</string>
<string name="dolby_preset_jazz">Jazz</string>
<string name="dolby_preset_pop">Pop</string>
<string name="dolby_preset_classical">Classical</string>
<string name="dolby_preset_hiphop">Hip Hop</string>
<string name="dolby_preset_blues">Blues</string>
<string name="dolby_preset_electronic">Electronic</string>
<string name="dolby_preset_country">Country</string>
<string name="dolby_preset_dance">Dance</string>
<string name="dolby_preset_metal">Metal</string>
<string name="dolby_preset_acoustic">Acoustic</string>
<string name="dolby_preset_bass">Bass Boost</string>
<string name="dolby_preset_beats">Beats</string>
<string name="dolby_preset_clear">Crystal Clear</string>
<string name="dolby_preset_deep">Deep</string>
<string name="dolby_preset_dubstep">Dubstep</string>
<string name="dolby_preset_hardstyle">Hardstyle</string>
<string name="dolby_preset_movie">Movie</string>
<string name="dolby_preset_rb">R&amp;B</string>
<string name="dolby_preset_vocal">Vocal Booster</string>
<string name="dolby_preset_custom">Custom</string>
<string name="equalizer">Equalizer</string>
<string name="advanced">Advanced</string>
<string name="home">Home</string>
<!-- Dolby equalizer UI -->
<string name="dolby_geq_slider_label_gain">Gain</string>
<string name="dolby_geq_preset">Preset</string>
<string name="dolby_geq_preset_name">Preset name</string>
<string name="dolby_geq_new_preset">New preset</string>
<string name="dolby_geq_rename_preset">Rename preset</string>
<string name="dolby_geq_delete_preset">Delete preset</string>
<string name="dolby_geq_delete_preset_prompt">Do you want to delete this preset?</string>
<string name="dolby_geq_reset_gains">Reset gains</string>
<string name="dolby_geq_reset_gains_prompt">Do you want to reset this preset to defaults?</string>
<string name="dolby_geq_preset_name_exists">Preset name already exists!</string>
<string name="dolby_geq_preset_name_too_long">Preset name is too long!</string>
<string name="band_mode_mismatch">Band Mode Mismatch</string>
<string name="frequency_response">Frequency Response</string>
<string name="locked">Locked</string>
<string name="band_configuration">Band Configuration</string>
<string name="choose_equalizer_precision">Choose equalizer precision: more bands = finer control</string>
<!-- Dolby intelligent EQ -->
<string name="dolby_ieq">Intelligent equalizer</string>
<string name="dolby_balanced">Balanced</string>
<string name="dolby_warm">Warm</string>
<string name="dolby_detailed">Detailed</string>
<!-- Per-App Audio Profiles -->
<string name="app_profiles_title">Per-App Audio Profiles</string>
<string name="app_profiles_desc">Assign different audio profiles to individual apps for optimized sound based on content type</string>
<string name="app_profiles_search_hint">Search apps...</string>
<string name="app_profiles_no_apps">No apps found</string>
<string name="app_profiles_loading">Loading apps...</string>
<string name="app_profiles_clear_all">Clear All App Profiles</string>
<string name="app_profiles_clear_all_message">This will remove all per-app profile assignments. Apps will use the default profile.</string>
<string name="app_profiles_manage">Manage App Profiles</string>
<string name="app_profiles_default">Default</string>
<string name="app_profiles_assigned">Assigned: %1$s</string>
<string name="app_profiles_headphone_only">Headphone/BT only mode</string>
<string name="app_profiles_headphone_only_description">Only switch profiles when earphones or Bluetooth audio devices are connected</string>
<string name="app_profiles_permission_required">Permission Required</string>
<string name="app_profiles_permission_required_details">To automatically switch profiles based on the active app, please grant Usage Access permission in the next screen.</string>
<string name="app_profiles_grant_permission">Grant Permission</string>
<string name="app_profiles_search_placeholder">Search apps...</string>
<string name="app_profiles_no_apps_found">No apps found</string>
<string name="app_profiles_retry">Retry</string>
<string name="app_profiles_change">Change</string>
<string name="app_profiles_auto_switch">Auto-switch profiles</string>
<string name="app_profiles_show_toasts">Show toasts</string>
<!-- Preset Import/Export -->
<string name="preset_import_export">Import/Export Presets</string>
<string name="preset_export_single">Export Preset</string>
<string name="preset_import_single">Import Preset</string>
<string name="preset_export_batch">Export All Presets</string>
<string name="preset_import_batch">Import Multiple Presets</string>
<string name="preset_copy_clipboard">Copy to Clipboard</string>
<string name="preset_paste_clipboard">Paste from Clipboard</string>
<string name="preset_share">Share Preset</string>
<string name="preset_export_success">Preset exported successfully</string>
<string name="preset_import_success">Preset imported successfully</string>
<string name="preset_export_failed">Export failed</string>
<string name="preset_import_failed">Import failed</string>
<!-- Notification Access -->
<string name="notification_access_required">Notification Access Required</string>
<string name="notification_access_required_desc">Enable notification access to keep Dolby Atmos running in the background and allow automatic profile switching.</string>
<string name="enable_notification_access">Enable Notification Access</string>
<string name="enable_notification_access_desc">Enable notification access to keep Dolby Atmos running in the background and allow automatic profile switching.</string>
<string name="notification_access_permission_details">This permission allows Dolby Atmos to:\n\n• Run continuously in the background\n• Automatically switch audio profiles\n• Monitor foreground apps\n\nYour notifications will not be read or modified.</string>
<string name="open_settings">Open Settings</string>
<string name="cancel">Cancel</string>
<!--import export -->
<string name="import_export_presets">Import/Export Presets</string>
<string name="import_presets">Import Presets</string>
<string name="import_presets_description">Import presets from files or clipboard</string>
<string name="import_single_file">Single File</string>
<string name="import_batch">Batch</string>
<string name="import_from_clipboard">From Clipboard</string>
<string name="your_custom_presets">Your Custom Presets</string>
<string name="processing">Processing...</string>
<string name="batch_export">Batch Export</string>
<string name="batch_export_description">Export all %1$s custom presets to a single file?</string>
<string name="export_all">Export All</string>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Dolby" parent="android:Theme.DeviceDefault.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="shared_presets" path="shared_presets/" />
</paths>

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.util.Log
import org.lunaris.dolby.data.DolbyRepository
import org.lunaris.dolby.service.AppProfileMonitorService
import org.lunaris.dolby.service.DolbyNotificationListener
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent: ${intent.action}")
when (intent.action) {
Intent.ACTION_LOCKED_BOOT_COMPLETED,
Intent.ACTION_BOOT_COMPLETED -> {
try {
val repository = DolbyRepository(context)
val prefs = context.getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
val enabled = prefs.getBoolean(DolbyConstants.PREF_ENABLE, false)
val savedProfile = prefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull() ?: 0
Log.d(TAG, "Boot restore - enabled: $enabled, profile: $savedProfile")
if (enabled) {
restoreProfileSettings(repository, context, savedProfile)
repository.setCurrentProfile(savedProfile)
repository.setDolbyEnabled(true)
Log.d(TAG, "Dolby restored successfully")
}
if (prefs.getBoolean("app_profile_monitoring_enabled", false)) {
AppProfileMonitorService.startMonitoring(context)
}
if (isNotificationListenerEnabled(context)) {
requestNotificationListenerRebind(context)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize Dolby", e)
}
}
}
}
private fun restoreProfileSettings(repository: DolbyRepository, context: Context, profile: Int) {
try {
val prefs = context.getSharedPreferences("profile_$profile", Context.MODE_PRIVATE)
val ieqPreset = prefs.getString(DolbyConstants.PREF_IEQ, "0")?.toIntOrNull() ?: 0
repository.setIeqPreset(profile, ieqPreset)
val hpVirtualizer = prefs.getBoolean(DolbyConstants.PREF_HP_VIRTUALIZER, false)
repository.setHeadphoneVirtualizerEnabled(profile, hpVirtualizer)
val spkVirtualizer = prefs.getBoolean(DolbyConstants.PREF_SPK_VIRTUALIZER, false)
repository.setSpeakerVirtualizerEnabled(profile, spkVirtualizer)
val stereoWidening = prefs.getInt(DolbyConstants.PREF_STEREO_WIDENING, 32)
repository.setStereoWideningAmount(profile, stereoWidening)
val dialogueEnabled = prefs.getBoolean(DolbyConstants.PREF_DIALOGUE, false)
repository.setDialogueEnhancerEnabled(profile, dialogueEnabled)
val dialogueAmount = prefs.getInt(DolbyConstants.PREF_DIALOGUE_AMOUNT, 6)
repository.setDialogueEnhancerAmount(profile, dialogueAmount)
val bassLevel = prefs.getInt(DolbyConstants.PREF_BASS_LEVEL, 0)
if (bassLevel > 0) {
repository.setBassLevel(profile, bassLevel)
}
val bassCurve = prefs.getInt(DolbyConstants.PREF_BASS_CURVE, 0)
repository.setBassCurve(profile, bassCurve)
val trebleLevel = prefs.getInt(DolbyConstants.PREF_TREBLE_LEVEL, 0)
if (trebleLevel > 0) {
repository.setTrebleLevel(profile, trebleLevel)
}
val volumeLeveler = prefs.getBoolean(DolbyConstants.PREF_VOLUME, false)
repository.setVolumeLevelerEnabled(profile, volumeLeveler)
Log.d(TAG, "Successfully restored all settings for profile $profile")
} catch (e: Exception) {
Log.e(TAG, "Failed to restore profile settings", e)
}
}
private fun isNotificationListenerEnabled(context: Context): Boolean {
val cn = ComponentName(context, DolbyNotificationListener::class.java)
val flat = Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners")
return flat?.contains(cn.flattenToString()) == true
}
private fun requestNotificationListenerRebind(context: Context) {
try {
val cn = ComponentName(context, DolbyNotificationListener::class.java)
DolbyNotificationListener::class.java.getMethod(
"requestRebind",
ComponentName::class.java
).invoke(null, cn)
Log.d(TAG, "Requested notification listener rebind")
} catch (e: Exception) {
Log.e(TAG, "Failed to request notification listener rebind", e)
}
}
companion object {
private const val TAG = "Dolby-Boot"
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby
import android.util.Log
object DolbyConstants {
const val TAG = "Dolby"
const val PREF_ENABLE = "dolby_enable"
const val PREF_PROFILE = "dolby_profile"
const val PREF_PRESET = "dolby_preset"
const val PREF_IEQ = "dolby_ieq"
const val PREF_HP_VIRTUALIZER = "dolby_virtualizer"
const val PREF_SPK_VIRTUALIZER = "dolby_spk_virtualizer"
const val PREF_STEREO_WIDENING = "dolby_stereo_widening"
const val PREF_DIALOGUE = "dolby_dialogue_enabled"
const val PREF_DIALOGUE_AMOUNT = "dolby_dialogue_amount"
const val PREF_BASS = "dolby_bass"
const val PREF_BASS_LEVEL = "dolby_bass_level"
const val PREF_BASS_CURVE = "dolby_bass_curve"
const val PREF_TREBLE = "dolby_treble"
const val PREF_TREBLE_LEVEL = "dolby_treble_level"
const val PREF_VOLUME = "dolby_volume"
const val PREF_PRESETS_MIGRATED = "presets_migrated"
const val PREF_BAND_MODE = "dolby_band_mode"
const val PREF_FILE_PRESETS = "presets"
enum class DsParam(val id: Int, val length: Int = 1) {
HEADPHONE_VIRTUALIZER(101),
SPEAKER_VIRTUALIZER(102),
VOLUME_LEVELER_ENABLE(103),
IEQ_PRESET(104),
DIALOGUE_ENHANCER_ENABLE(105),
DIALOGUE_ENHANCER_AMOUNT(108),
GEQ_BAND_GAINS(110, 20),
BASS_ENHANCER_ENABLE(111),
STEREO_WIDENING_AMOUNT(113);
override fun toString(): String = "${name}(${id})"
}
fun dlog(tag: String, msg: String) {
if (Log.isLoggable(TAG, Log.DEBUG) || Log.isLoggable(tag, Log.DEBUG)) {
Log.d("$TAG-$tag", msg)
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2023-24 Paranoid Android
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.audio
import android.media.audiofx.AudioEffect
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.DolbyConstants.DsParam
import java.util.UUID
class DolbyAudioEffect(priority: Int, audioSession: Int) : AudioEffect(
EFFECT_TYPE_NULL, EFFECT_TYPE_DAP, priority, audioSession
) {
var dsOn: Boolean
get() = getIntParam(EFFECT_PARAM_ENABLE) == 1
set(value) {
setIntParam(EFFECT_PARAM_ENABLE, if (value) 1 else 0)
enabled = value
}
var profile: Int
get() = getIntParam(EFFECT_PARAM_PROFILE)
set(value) {
setIntParam(EFFECT_PARAM_PROFILE, value)
}
private fun setIntParam(param: Int, value: Int) {
DolbyConstants.dlog(TAG, "setIntParam($param, $value)")
val buf = ByteArray(12)
int32ToByteArray(param, buf, 0)
int32ToByteArray(1, buf, 4)
int32ToByteArray(value, buf, 8)
checkStatus(setParameter(EFFECT_PARAM_CPDP_VALUES, buf))
}
private fun getIntParam(param: Int): Int {
val buf = ByteArray(12)
int32ToByteArray(param, buf, 0)
checkStatus(getParameter(EFFECT_PARAM_CPDP_VALUES + param, buf))
return byteArrayToInt32(buf).also {
DolbyConstants.dlog(TAG, "getIntParam($param): $it")
}
}
fun resetProfileSpecificSettings(profile: Int = this.profile) {
DolbyConstants.dlog(TAG, "resetProfileSpecificSettings: profile=$profile")
setIntParam(EFFECT_PARAM_RESET_PROFILE_SETTINGS, profile)
}
fun setDapParameter(param: DsParam, values: IntArray, profile: Int = this.profile) {
DolbyConstants.dlog(TAG, "setDapParameter: profile=$profile param=$param")
val length = values.size
val buf = ByteArray((length + 4) * 4)
int32ToByteArray(EFFECT_PARAM_SET_PROFILE_PARAMETER, buf, 0)
int32ToByteArray(length + 1, buf, 4)
int32ToByteArray(profile, buf, 8)
int32ToByteArray(param.id, buf, 12)
int32ArrayToByteArray(values, buf, 16)
checkStatus(setParameter(EFFECT_PARAM_CPDP_VALUES, buf))
}
fun setDapParameter(param: DsParam, enable: Boolean, profile: Int = this.profile) =
setDapParameter(param, intArrayOf(if (enable) 1 else 0), profile)
fun setDapParameter(param: DsParam, value: Int, profile: Int = this.profile) =
setDapParameter(param, intArrayOf(value), profile)
fun getDapParameter(param: DsParam, profile: Int = this.profile): IntArray {
DolbyConstants.dlog(TAG, "getDapParameter: profile=$profile param=$param")
val length = param.length
val buf = ByteArray((length + 2) * 4)
val p = (param.id shl 16) + (profile shl 8) + EFFECT_PARAM_GET_PROFILE_PARAMETER
checkStatus(getParameter(p, buf))
return byteArrayToInt32Array(buf, length)
}
fun getDapParameterBool(param: DsParam, profile: Int = this.profile): Boolean =
getDapParameter(param, profile)[0] == 1
fun getDapParameterInt(param: DsParam, profile: Int = this.profile): Int =
getDapParameter(param, profile)[0]
companion object {
private const val TAG = "DolbyAudioEffect"
private val EFFECT_TYPE_DAP = UUID.fromString("9d4921da-8225-4f29-aefa-39537a04bcaa")
private const val EFFECT_PARAM_ENABLE = 0
private const val EFFECT_PARAM_CPDP_VALUES = 5
private const val EFFECT_PARAM_PROFILE = 0xA000000
private const val EFFECT_PARAM_SET_PROFILE_PARAMETER = 0x1000000
private const val EFFECT_PARAM_GET_PROFILE_PARAMETER = 0x1000005
private const val EFFECT_PARAM_RESET_PROFILE_SETTINGS = 0xC000000
private fun int32ToByteArray(value: Int, dst: ByteArray, index: Int) {
var idx = index
dst[idx++] = (value and 0xff).toByte()
dst[idx++] = ((value ushr 8) and 0xff).toByte()
dst[idx++] = ((value ushr 16) and 0xff).toByte()
dst[idx] = ((value ushr 24) and 0xff).toByte()
}
private fun byteArrayToInt32(ba: ByteArray): Int {
return ((ba[3].toInt() and 0xff) shl 24) or
((ba[2].toInt() and 0xff) shl 16) or
((ba[1].toInt() and 0xff) shl 8) or
(ba[0].toInt() and 0xff)
}
private fun int32ArrayToByteArray(src: IntArray, dst: ByteArray, index: Int) {
var idx = index
for (x in src) {
dst[idx++] = (x and 0xff).toByte()
dst[idx++] = ((x ushr 8) and 0xff).toByte()
dst[idx++] = ((x ushr 16) and 0xff).toByte()
dst[idx++] = ((x ushr 24) and 0xff).toByte()
}
}
private fun byteArrayToInt32Array(ba: ByteArray, dstLength: Int): IntArray {
val srcLength = ba.size shr 2
val dst = IntArray(dstLength.coerceAtMost(srcLength))
for (i in dst.indices) {
dst[i] = ((ba[i * 4 + 3].toInt() and 0xff) shl 24) or
((ba[i * 4 + 2].toInt() and 0xff) shl 16) or
((ba[i * 4 + 1].toInt() and 0xff) shl 8) or
(ba[i * 4].toInt() and 0xff)
}
return dst
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.data
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
data class AppInfo(
val packageName: String,
val appName: String,
val icon: Drawable?,
val assignedProfile: Int = -1
)
class AppProfileManager(private val context: Context) {
private val prefs = context.getSharedPreferences("app_profiles", Context.MODE_PRIVATE)
private val packageManager = context.packageManager
suspend fun getInstalledApps(): List<AppInfo> = withContext(Dispatchers.IO) {
try {
val mainIntent = Intent(Intent.ACTION_MAIN, null).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
}
val resolveInfos = packageManager.queryIntentActivities(mainIntent, 0)
val assignedProfiles = getAppsWithProfiles()
resolveInfos
.distinctBy { it.activityInfo.packageName }
.map { resolveInfo ->
val packageName = resolveInfo.activityInfo.packageName
AppInfo(
packageName = packageName,
appName = resolveInfo.loadLabel(packageManager).toString(),
icon = resolveInfo.loadIcon(packageManager),
assignedProfile = assignedProfiles[packageName] ?: -1
)
}
.sortedBy { it.appName }
} catch (e: Exception) {
emptyList()
}
}
fun getAppProfile(packageName: String): Int {
return prefs.getInt(packageName, -1)
}
fun setAppProfile(packageName: String, profile: Int) {
prefs.edit().putInt(packageName, profile).apply()
}
fun removeAppProfile(packageName: String) {
prefs.edit().remove(packageName).apply()
}
fun getAppsWithProfiles(): Map<String, Int> {
return prefs.all.mapNotNull { (key, value) ->
if (value is Int) key to value else null
}.toMap()
}
fun clearAllAppProfiles() {
prefs.edit().clear().apply()
}
}

View File

@@ -0,0 +1,819 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.data
import android.content.Context
import android.content.SharedPreferences
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.DolbyConstants.DsParam
import org.lunaris.dolby.R
import org.lunaris.dolby.audio.DolbyAudioEffect
import org.lunaris.dolby.domain.models.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class DolbyRepository(private val context: Context) : AutoCloseable {
private val audioManager = context.getSystemService(AudioManager::class.java)
private var dolbyEffect = createDolbyEffect()
private val defaultPrefs = context.getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
private val presetsPrefs = context.getSharedPreferences(DolbyConstants.PREF_FILE_PRESETS, Context.MODE_PRIVATE)
private val _isOnSpeaker = MutableStateFlow(checkIsOnSpeaker())
val isOnSpeaker: StateFlow<Boolean> = _isOnSpeaker.asStateFlow()
private val _currentProfile = MutableStateFlow(0)
val currentProfile: StateFlow<Int> = _currentProfile.asStateFlow()
val stereoWideningSupported = context.resources.getBoolean(R.bool.dolby_stereo_widening_supported)
val volumeLevelerSupported = context.resources.getBoolean(R.bool.dolby_volume_leveler_supported)
private var isReleased = false
private var cachedPresets: List<EqualizerPreset>? = null
private val presetCacheLock = Any()
private fun createDolbyEffect(): DolbyAudioEffect {
return try {
DolbyAudioEffect(EFFECT_PRIORITY, audioSession = 0)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Failed to create Dolby effect: ${e.message}")
throw e
}
}
private fun checkEffect() {
if (isReleased) {
DolbyConstants.dlog(TAG, "Repository released, skipping effect check")
return
}
try {
if (!dolbyEffect.hasControl()) {
DolbyConstants.dlog(TAG, "Lost audio effect control, recreating")
dolbyEffect.release()
dolbyEffect = createDolbyEffect()
}
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error checking effect: ${e.message}")
}
}
private fun checkIsOnSpeaker(): Boolean {
return try {
val device = audioManager.getDevicesForAttributes(ATTRIBUTES_MEDIA)[0]
device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error checking speaker state: ${e.message}")
false
}
}
fun updateSpeakerState() {
if (!isReleased) {
_isOnSpeaker.value = checkIsOnSpeaker()
}
}
fun getDolbyEnabled(): Boolean {
return try {
dolbyEffect.dsOn
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting Dolby enabled state: ${e.message}")
false
}
}
fun setDolbyEnabled(enabled: Boolean) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.dsOn = enabled
defaultPrefs.edit().putBoolean(DolbyConstants.PREF_ENABLE, enabled).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting Dolby enabled: ${e.message}")
}
}
fun getCurrentProfile(): Int {
return try {
dolbyEffect.profile
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting current profile: ${e.message}")
0
}
}
fun setCurrentProfile(profile: Int) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.profile = profile
defaultPrefs.edit().putString(DolbyConstants.PREF_PROFILE, profile.toString()).apply()
if (!verifyProfileSaved(profile)) {
DolbyConstants.dlog(TAG, "WARNING: Profile may not have been saved correctly!")
}
restoreProfilePreset(profile)
_currentProfile.value = profile
DolbyConstants.dlog(TAG, "Profile set to: $profile")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting current profile: ${e.message}")
}
}
private fun restoreProfilePreset(profile: Int) {
try {
val prefs = getProfilePrefs(profile)
val savedPresetGains = prefs.getString(DolbyConstants.PREF_PRESET, null)
if (savedPresetGains != null) {
val gains = savedPresetGains.split(",").mapNotNull { it.toIntOrNull() }.toIntArray()
if (gains.size == 20) {
dolbyEffect.setDapParameter(DsParam.GEQ_BAND_GAINS, gains, profile)
DolbyConstants.dlog(TAG, "Restored preset for profile $profile")
}
}
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Failed to restore preset for profile $profile: ${e.message}")
}
}
fun verifyProfileSaved(profile: Int): Boolean {
val prefs = defaultPrefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull()
val saved = prefs == profile
DolbyConstants.dlog(TAG, "Profile verification: requested=$profile, saved=$prefs, match=$saved")
return saved
}
private fun getProfilePrefs(profile: Int): SharedPreferences {
return context.getSharedPreferences("profile_$profile", Context.MODE_PRIVATE)
}
fun getBandMode(): BandMode {
val mode = defaultPrefs.getString(DolbyConstants.PREF_BAND_MODE, "10")
return when (mode) {
"10" -> BandMode.TEN_BAND
"15" -> BandMode.FIFTEEN_BAND
"20" -> BandMode.TWENTY_BAND
else -> BandMode.TEN_BAND
}
}
fun setBandMode(mode: BandMode) {
defaultPrefs.edit().putString(DolbyConstants.PREF_BAND_MODE, mode.value).apply()
}
fun getBassEnhancerEnabled(profile: Int): Boolean {
return try {
dolbyEffect.getDapParameterBool(DsParam.BASS_ENHANCER_ENABLE, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting bass enhancer: ${e.message}")
false
}
}
fun setBassEnhancerEnabled(profile: Int, enabled: Boolean) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.BASS_ENHANCER_ENABLE, enabled, profile)
getProfilePrefs(profile).edit().putBoolean(DolbyConstants.PREF_BASS, enabled).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting bass enhancer: ${e.message}")
}
}
fun getBassLevel(profile: Int): Int {
val prefs = getProfilePrefs(profile)
return prefs.getInt(DolbyConstants.PREF_BASS_LEVEL, 0)
}
fun getBassCurve(profile: Int): Int {
val prefs = getProfilePrefs(profile)
return prefs.getInt(DolbyConstants.PREF_BASS_CURVE, 0)
}
fun setBassCurve(profile: Int, curve: Int) {
if (isReleased) return
try {
val prefs = getProfilePrefs(profile)
val previousCurve = prefs.getInt(DolbyConstants.PREF_BASS_CURVE, 0)
val level = prefs.getInt(DolbyConstants.PREF_BASS_LEVEL, 0)
if (previousCurve == curve) return
prefs.edit().putInt(DolbyConstants.PREF_BASS_CURVE, curve).apply()
if (level <= 0) return
checkEffect()
val currentGains = dolbyEffect.getDapParameter(DsParam.GEQ_BAND_GAINS, profile)
val modifiedGains = currentGains.copyOf()
applyBassCurve(modifiedGains, level, previousCurve, -1)
applyBassCurve(modifiedGains, level, curve, 1)
dolbyEffect.setDapParameter(DsParam.GEQ_BAND_GAINS, modifiedGains, profile)
val gainsString = modifiedGains.joinToString(",")
prefs.edit().putString(DolbyConstants.PREF_PRESET, gainsString).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting bass curve: ${e.message}")
throw e
}
}
private fun applyBassCurve(gains: IntArray, level: Int, curve: Int, direction: Int) {
val weights = BASS_CURVES.getOrElse(curve) { BASS_CURVES[0] }
val baseGain = level * BASS_GAIN_MULTIPLIER
for (i in weights.indices) {
if (i >= gains.size) break
val weightedGain = (baseGain * weights[i] * direction).toInt()
gains[i] = (gains[i] + weightedGain).coerceIn(-150, 150)
}
}
fun setBassLevel(profile: Int, level: Int) {
if (isReleased) return
DolbyConstants.dlog(TAG, "setBassLevel: profile=$profile level=$level")
if (level !in 0..100) {
DolbyConstants.dlog(TAG, "setBassLevel: invalid level $level")
throw IllegalArgumentException("Bass level must be between 0 and 100")
}
try {
val prefs = getProfilePrefs(profile)
val previousLevel = prefs.getInt(DolbyConstants.PREF_BASS_LEVEL, 0)
prefs.edit().putInt(DolbyConstants.PREF_BASS_LEVEL, level).apply()
setBassEnhancerEnabled(profile, level > 0)
checkEffect()
val currentGains = dolbyEffect.getDapParameter(DsParam.GEQ_BAND_GAINS, profile)
val modifiedGains = currentGains.copyOf()
val curve = prefs.getInt(DolbyConstants.PREF_BASS_CURVE, 0)
if (previousLevel > 0) {
applyBassCurve(modifiedGains, previousLevel, curve, -1)
}
if (level > 0) {
applyBassCurve(modifiedGains, level, curve, 1)
}
dolbyEffect.setDapParameter(DsParam.GEQ_BAND_GAINS, modifiedGains, profile)
val gainsString = modifiedGains.joinToString(",")
prefs.edit().putString(DolbyConstants.PREF_PRESET, gainsString).apply()
DolbyConstants.dlog(TAG, "setBassLevel: success")
} catch (e: IllegalArgumentException) {
DolbyConstants.dlog(TAG, "setBassLevel: validation error - ${e.message}")
val prefs = getProfilePrefs(profile)
prefs.edit().putInt(DolbyConstants.PREF_BASS_LEVEL, 0).apply()
throw e
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "setBassLevel: unexpected error - ${e.message}")
throw e
}
}
fun getTrebleEnhancerEnabled(profile: Int): Boolean {
val prefs = getProfilePrefs(profile)
return prefs.getBoolean(DolbyConstants.PREF_TREBLE, false)
}
fun setTrebleEnhancerEnabled(profile: Int, enabled: Boolean) {
getProfilePrefs(profile).edit().putBoolean(DolbyConstants.PREF_TREBLE, enabled).apply()
}
fun getTrebleLevel(profile: Int): Int {
val prefs = getProfilePrefs(profile)
return prefs.getInt(DolbyConstants.PREF_TREBLE_LEVEL, 0)
}
fun setTrebleLevel(profile: Int, level: Int) {
if (isReleased) return
DolbyConstants.dlog(TAG, "setTrebleLevel: profile=$profile level=$level")
if (level !in 0..100) {
DolbyConstants.dlog(TAG, "setTrebleLevel: invalid level $level")
throw IllegalArgumentException("Treble level must be between 0 and 100")
}
try {
val prefs = getProfilePrefs(profile)
val previousLevel = prefs.getInt(DolbyConstants.PREF_TREBLE_LEVEL, 0)
prefs.edit().putInt(DolbyConstants.PREF_TREBLE_LEVEL, level).apply()
setTrebleEnhancerEnabled(profile, level > 0)
checkEffect()
val currentGains = dolbyEffect.getDapParameter(DsParam.GEQ_BAND_GAINS, profile)
val modifiedGains = currentGains.copyOf()
if (previousLevel > 0) {
val previousGain = (previousLevel * TREBLE_GAIN_MULTIPLIER).toInt()
for (i in 14..19) {
if (i < modifiedGains.size) {
modifiedGains[i] = (modifiedGains[i] - previousGain).coerceIn(-150, 150)
}
}
}
if (level > 0) {
val trebleGain = (level * TREBLE_GAIN_MULTIPLIER).toInt()
for (i in 14..19) {
if (i < modifiedGains.size) {
modifiedGains[i] = (modifiedGains[i] + trebleGain).coerceIn(-150, 150)
}
}
}
dolbyEffect.setDapParameter(DsParam.GEQ_BAND_GAINS, modifiedGains, profile)
val gainsString = modifiedGains.joinToString(",")
prefs.edit().putString(DolbyConstants.PREF_PRESET, gainsString).apply()
DolbyConstants.dlog(TAG, "setTrebleLevel: success")
} catch (e: IllegalArgumentException) {
DolbyConstants.dlog(TAG, "setTrebleLevel: validation error - ${e.message}")
val prefs = getProfilePrefs(profile)
prefs.edit().putInt(DolbyConstants.PREF_TREBLE_LEVEL, 0).apply()
throw e
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "setTrebleLevel: unexpected error - ${e.message}")
throw e
}
}
fun getVolumeLevelerEnabled(profile: Int): Boolean {
if (!volumeLevelerSupported) return false
return try {
dolbyEffect.getDapParameterBool(DsParam.VOLUME_LEVELER_ENABLE, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting volume leveler: ${e.message}")
false
}
}
fun setVolumeLevelerEnabled(profile: Int, enabled: Boolean) {
if (!volumeLevelerSupported || isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.VOLUME_LEVELER_ENABLE, enabled, profile)
getProfilePrefs(profile).edit().putBoolean(DolbyConstants.PREF_VOLUME, enabled).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting volume leveler: ${e.message}")
}
}
fun getIeqPreset(profile: Int): Int {
return try {
dolbyEffect.getDapParameterInt(DsParam.IEQ_PRESET, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting IEQ preset: ${e.message}")
0
}
}
fun setIeqPreset(profile: Int, preset: Int) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.IEQ_PRESET, preset, profile)
getProfilePrefs(profile).edit().putString(DolbyConstants.PREF_IEQ, preset.toString()).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting IEQ preset: ${e.message}")
}
}
fun getHeadphoneVirtualizerEnabled(profile: Int): Boolean {
return try {
dolbyEffect.getDapParameterBool(DsParam.HEADPHONE_VIRTUALIZER, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting headphone virtualizer: ${e.message}")
false
}
}
fun setHeadphoneVirtualizerEnabled(profile: Int, enabled: Boolean) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.HEADPHONE_VIRTUALIZER, enabled, profile)
getProfilePrefs(profile).edit().putBoolean(DolbyConstants.PREF_HP_VIRTUALIZER, enabled).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting headphone virtualizer: ${e.message}")
}
}
fun getSpeakerVirtualizerEnabled(profile: Int): Boolean {
return try {
dolbyEffect.getDapParameterBool(DsParam.SPEAKER_VIRTUALIZER, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting speaker virtualizer: ${e.message}")
false
}
}
fun setSpeakerVirtualizerEnabled(profile: Int, enabled: Boolean) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.SPEAKER_VIRTUALIZER, enabled, profile)
getProfilePrefs(profile).edit().putBoolean(DolbyConstants.PREF_SPK_VIRTUALIZER, enabled).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting speaker virtualizer: ${e.message}")
}
}
fun getStereoWideningAmount(profile: Int): Int {
if (!stereoWideningSupported) return 0
return try {
dolbyEffect.getDapParameterInt(DsParam.STEREO_WIDENING_AMOUNT, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting stereo widening: ${e.message}")
32
}
}
fun setStereoWideningAmount(profile: Int, amount: Int) {
if (!stereoWideningSupported || isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.STEREO_WIDENING_AMOUNT, amount, profile)
getProfilePrefs(profile).edit().putInt(DolbyConstants.PREF_STEREO_WIDENING, amount).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting stereo widening: ${e.message}")
}
}
fun getDialogueEnhancerEnabled(profile: Int): Boolean {
return try {
dolbyEffect.getDapParameterBool(DsParam.DIALOGUE_ENHANCER_ENABLE, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting dialogue enhancer: ${e.message}")
false
}
}
fun setDialogueEnhancerEnabled(profile: Int, enabled: Boolean) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.DIALOGUE_ENHANCER_ENABLE, enabled, profile)
getProfilePrefs(profile).edit().putBoolean(DolbyConstants.PREF_DIALOGUE, enabled).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting dialogue enhancer: ${e.message}")
}
}
fun getDialogueEnhancerAmount(profile: Int): Int {
return try {
dolbyEffect.getDapParameterInt(DsParam.DIALOGUE_ENHANCER_AMOUNT, profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting dialogue enhancer amount: ${e.message}")
6
}
}
fun setDialogueEnhancerAmount(profile: Int, amount: Int) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.setDapParameter(DsParam.DIALOGUE_ENHANCER_AMOUNT, amount, profile)
getProfilePrefs(profile).edit().putInt(DolbyConstants.PREF_DIALOGUE_AMOUNT, amount).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting dialogue enhancer amount: ${e.message}")
}
}
fun getEqualizerGains(profile: Int, bandMode: BandMode): List<BandGain> {
return try {
val gains = dolbyEffect.getDapParameter(DsParam.GEQ_BAND_GAINS, profile)
deserializeGains(gains, bandMode)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting equalizer gains: ${e.message}")
val frequencies = when (bandMode) {
BandMode.TEN_BAND -> BAND_FREQUENCIES_10
BandMode.FIFTEEN_BAND -> BAND_FREQUENCIES_15
BandMode.TWENTY_BAND -> BAND_FREQUENCIES_20
}
frequencies.map { BandGain(frequency = it, gain = 0) }
}
}
fun setEqualizerGains(profile: Int, bandGains: List<BandGain>, bandMode: BandMode) {
if (isReleased) return
try {
checkEffect()
val gains = serializeGains(bandGains, bandMode)
dolbyEffect.setDapParameter(DsParam.GEQ_BAND_GAINS, gains, profile)
val gainsString = gains.joinToString(",")
getProfilePrefs(profile).edit().putString(DolbyConstants.PREF_PRESET, gainsString).apply()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting equalizer gains: ${e.message}")
}
}
fun getPresetName(profile: Int): String {
return try {
val gains = dolbyEffect.getDapParameter(DsParam.GEQ_BAND_GAINS, profile)
val tenBandGains = gains.filterIndexed { index, _ -> index % 2 == 0 }
val currentGainsString = tenBandGains.joinToString(",")
val presetValues = context.resources.getStringArray(R.array.dolby_preset_values)
val presetNames = context.resources.getStringArray(R.array.dolby_preset_entries)
presetValues.forEachIndexed { index, preset ->
val presetTenBand = convertTo10Band(preset)
if (gainsMatch(presetTenBand, currentGainsString)) {
return presetNames[index]
}
}
presetsPrefs.all.forEach { (name, value) ->
val presetTenBand = convertTo10Band(value.toString())
if (gainsMatch(presetTenBand, currentGainsString)) {
return name
}
}
context.getString(R.string.dolby_preset_custom)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting preset name: ${e.message}")
context.getString(R.string.dolby_preset_custom)
}
}
private fun convertTo10Band(gainsString: String): String {
val gains = gainsString.split(",").map { it.trim().toIntOrNull() ?: 0 }
if (gains.size == 10) {
return gains.joinToString(",")
}
if (gains.size == 20) {
val tenBand = gains.filterIndexed { index, _ -> index % 2 == 0 }
return tenBand.joinToString(",")
}
return gainsString
}
private fun gainsMatch(gains1: String, gains2: String): Boolean {
val g1 = gains1.split(",").map { it.trim().toIntOrNull() ?: 0 }
val g2 = gains2.split(",").map { it.trim().toIntOrNull() ?: 0 }
if (g1.size != g2.size) return false
return g1.zip(g2).all { (a, b) -> kotlin.math.abs(a - b) <= 1 }
}
fun getUserPresets(): List<EqualizerPreset> {
synchronized(presetCacheLock) {
cachedPresets?.let { return it }
val bandMode = getBandMode()
val presets = presetsPrefs.all.mapNotNull { (name, value) ->
try {
val valueStr = value as? String ?: return@mapNotNull null
parsePreset(name, valueStr)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error parsing preset $name: ${e.message}")
null
}
}
cachedPresets = presets
return presets
}
}
private fun parsePreset(name: String, valueStr: String): EqualizerPreset? {
return try {
if (valueStr.contains("|")) {
val parts = valueStr.split("|")
val presetBandMode = BandMode.fromValue(parts[1])
val gains = parts[0].split(",").map { it.toInt() }.toIntArray()
EqualizerPreset(
name = name,
bandGains = deserializeGains(gains, presetBandMode),
isUserDefined = true,
bandMode = presetBandMode
)
} else {
val gains = valueStr.split(",").map { it.toInt() }.toIntArray()
EqualizerPreset(
name = name,
bandGains = deserializeGains(gains, BandMode.TEN_BAND),
isUserDefined = true,
bandMode = BandMode.TEN_BAND
)
}
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error parsing preset value: ${e.message}")
null
}
}
fun addUserPreset(name: String, bandGains: List<BandGain>, bandMode: BandMode) {
try {
val gains = serializeGains(bandGains, bandMode).joinToString(",")
val value = "$gains|${bandMode.value}"
presetsPrefs.edit().putString(name, value).apply()
synchronized(presetCacheLock) {
cachedPresets = null
}
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error adding user preset: ${e.message}")
}
}
fun deleteUserPreset(name: String) {
try {
presetsPrefs.edit().remove(name).apply()
synchronized(presetCacheLock) {
cachedPresets = null
}
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error deleting user preset: ${e.message}")
}
}
fun resetProfile(profile: Int) {
if (isReleased) return
try {
checkEffect()
dolbyEffect.resetProfileSpecificSettings(profile)
context.deleteSharedPreferences("profile_$profile")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error resetting profile: ${e.message}")
}
}
fun resetAllProfiles() {
if (isReleased) return
try {
checkEffect()
context.resources.getStringArray(R.array.dolby_profile_values)
.map { it.toInt() }
.forEach { resetProfile(it) }
setCurrentProfile(0)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error resetting all profiles: ${e.message}")
}
}
private fun deserializeGains(gains: IntArray, bandMode: BandMode): List<BandGain> {
val frequencies = when (bandMode) {
BandMode.TEN_BAND -> BAND_FREQUENCIES_10
BandMode.FIFTEEN_BAND -> BAND_FREQUENCIES_15
BandMode.TWENTY_BAND -> BAND_FREQUENCIES_20
}
val indices = when (bandMode) {
BandMode.TEN_BAND -> TEN_BAND_INDICES
BandMode.FIFTEEN_BAND -> FIFTEEN_BAND_INDICES
BandMode.TWENTY_BAND -> (0..19).toList()
}
return frequencies.mapIndexed { index, freq ->
val gainIndex = indices.getOrNull(index) ?: index
BandGain(frequency = freq, gain = gains.getOrElse(gainIndex) { 0 })
}
}
private fun serializeGains(bandGains: List<BandGain>, bandMode: BandMode): IntArray {
val result = IntArray(20) { 0 }
when (bandMode) {
BandMode.TEN_BAND -> {
TEN_BAND_INDICES.forEachIndexed { index, targetIndex ->
if (index < bandGains.size && targetIndex < 20) {
result[targetIndex] = bandGains[index].gain
}
}
for (i in 0 until 19 step 2) {
if (i + 2 < 20) {
result[i + 1] = (result[i] + result[i + 2]) / 2
}
}
result[19] = result[18]
}
BandMode.FIFTEEN_BAND -> {
FIFTEEN_BAND_INDICES.forEachIndexed { index, targetIndex ->
if (index < bandGains.size && targetIndex < 20) {
result[targetIndex] = bandGains[index].gain
}
}
val missing = (0..19).filter { it !in FIFTEEN_BAND_INDICES }
missing.forEach { idx ->
val prev = FIFTEEN_BAND_INDICES.filter { it < idx }.maxOrNull() ?: 0
val next = FIFTEEN_BAND_INDICES.filter { it > idx }.minOrNull() ?: 19
if (prev < idx && next > idx && prev < 20 && next < 20) {
val prevValue = result[prev]
val nextValue = result[next]
val ratio = (idx - prev).toFloat() / (next - prev)
result[idx] = (prevValue + ratio * (nextValue - prevValue)).toInt()
} else if (prev < 20) {
result[idx] = result[prev]
}
}
}
BandMode.TWENTY_BAND -> {
bandGains.forEachIndexed { index, bandGain ->
if (index < 20) {
result[index] = bandGain.gain
}
}
}
}
return result
}
private fun release() {
if (!isReleased) {
DolbyConstants.dlog(TAG, "Releasing repository resources")
isReleased = true
try {
dolbyEffect.release()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error releasing effect: ${e.message}")
}
}
}
override fun close() {
release()
}
companion object {
private const val TAG = "DolbyRepository"
private const val EFFECT_PRIORITY = 100
private const val BASS_GAIN_MULTIPLIER = 1.4f
private const val TREBLE_GAIN_MULTIPLIER = 1.5f
private val ATTRIBUTES_MEDIA = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
val BAND_FREQUENCIES_10 = listOf(32, 64, 125, 250, 500, 1000, 2250, 5000, 10000, 19688)
val BAND_FREQUENCIES_15 = listOf(
32, 47, 94, 141, 234, 469, 844, 1313, 2250, 3750, 5813, 9000, 11250, 13875, 19688
)
val BAND_FREQUENCIES_20 = listOf(
32, 47, 141, 234, 328, 469, 656, 844, 1031, 1313,
1688, 2250, 3000, 3750, 4688, 5813, 7125, 9000, 11250, 19688
)
private val TEN_BAND_INDICES = listOf(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
private val FIFTEEN_BAND_INDICES = listOf(0, 1, 2, 3, 4, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19)
private val BASS_CURVES = listOf(
floatArrayOf(
1.00f, 1.00f, 0.95f, 0.90f, 0.80f, 0.70f, 0.55f, 0.40f, 0.25f, 0.15f,
0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f
),
floatArrayOf(
1.20f, 1.15f, 1.05f, 0.90f, 0.70f, 0.55f, 0.40f, 0.25f, 0.10f, 0.05f,
0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f
),
floatArrayOf(
0.90f, 0.95f, 1.00f, 1.00f, 0.90f, 0.75f, 0.60f, 0.45f, 0.30f, 0.20f,
0.10f, 0.05f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f
)
)
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.data
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import org.lunaris.dolby.domain.models.BandGain
import org.lunaris.dolby.domain.models.BandMode
import org.lunaris.dolby.domain.models.EqualizerPreset
import java.io.*
class PresetExportManager(private val context: Context) {
companion object {
private const val PRESET_FILE_VERSION = 2
private const val FILE_EXTENSION = ".ldp"
private const val MIME_TYPE = "application/json"
}
suspend fun exportPresetToJson(preset: EqualizerPreset): String = withContext(Dispatchers.IO) {
val json = JSONObject().apply {
put("version", PRESET_FILE_VERSION)
put("name", preset.name)
put("timestamp", System.currentTimeMillis())
put("createdBy", "Lunaris Dolby Manager")
put("bandMode", preset.bandMode.value)
put("bandCount", preset.bandMode.bandCount)
val gainsArray = JSONArray()
preset.bandGains.forEach { bandGain ->
gainsArray.put(JSONObject().apply {
put("frequency", bandGain.frequency)
put("gain", bandGain.gain)
})
}
put("bandGains", gainsArray)
}
json.toString(2)
}
suspend fun importPresetFromJson(jsonString: String): EqualizerPreset = withContext(Dispatchers.IO) {
val json = JSONObject(jsonString)
val version = json.optInt("version", 1)
if (version > PRESET_FILE_VERSION) {
throw IllegalArgumentException("Preset version not supported")
}
val name = json.getString("name")
val bandMode = if (json.has("bandMode")) {
BandMode.fromValue(json.getString("bandMode"))
} else {
BandMode.TEN_BAND
}
val gainsArray = json.getJSONArray("bandGains")
val bandGains = mutableListOf<BandGain>()
for (i in 0 until gainsArray.length()) {
val gainObj = gainsArray.getJSONObject(i)
bandGains.add(BandGain(
frequency = gainObj.getInt("frequency"),
gain = gainObj.getInt("gain")
))
}
val expectedCount = bandMode.bandCount
if (bandGains.size != expectedCount) {
throw IllegalArgumentException(
"Band count mismatch: expected $expectedCount, got ${bandGains.size}"
)
}
EqualizerPreset(
name = name,
bandGains = bandGains,
isUserDefined = true,
bandMode = bandMode
)
}
suspend fun exportPresetToFile(preset: EqualizerPreset, uri: Uri): Result<Unit> =
withContext(Dispatchers.IO) {
try {
val json = exportPresetToJson(preset)
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
BufferedWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)).use { writer ->
writer.write(json)
}
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun importPresetFromFile(uri: Uri): Result<EqualizerPreset> =
withContext(Dispatchers.IO) {
try {
val json = context.contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream, Charsets.UTF_8)).use { reader ->
reader.readText()
}
} ?: throw IOException("Cannot open file")
val preset = importPresetFromJson(json)
Result.success(preset)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun exportMultiplePresets(
presets: List<EqualizerPreset>,
uri: Uri
): Result<Unit> = withContext(Dispatchers.IO) {
try {
val json = JSONObject().apply {
put("version", PRESET_FILE_VERSION)
put("count", presets.size)
put("timestamp", System.currentTimeMillis())
put("supportsBandModes", true)
val presetsArray = JSONArray()
presets.forEach { preset ->
presetsArray.put(JSONObject(exportPresetToJson(preset)))
}
put("presets", presetsArray)
}
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
BufferedWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)).use { writer ->
writer.write(json.toString(2))
}
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun importMultiplePresets(uri: Uri): Result<List<EqualizerPreset>> =
withContext(Dispatchers.IO) {
try {
val json = context.contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream, Charsets.UTF_8)).use { reader ->
reader.readText()
}
} ?: throw IOException("Cannot open file")
val jsonObject = JSONObject(json)
val presetsArray = jsonObject.getJSONArray("presets")
val presets = mutableListOf<EqualizerPreset>()
for (i in 0 until presetsArray.length()) {
val presetJson = presetsArray.getJSONObject(i).toString()
presets.add(importPresetFromJson(presetJson))
}
Result.success(presets)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createShareIntent(preset: EqualizerPreset): Result<android.content.Intent> =
withContext(Dispatchers.IO) {
try {
val json = exportPresetToJson(preset)
val fileName = "${preset.name.replace(" ", "_")}_${preset.bandMode.value}band$FILE_EXTENSION"
val cacheDir = File(context.cacheDir, "shared_presets")
cacheDir.mkdirs()
val file = File(cacheDir, fileName)
FileOutputStream(file).use { fos ->
BufferedWriter(OutputStreamWriter(fos, Charsets.UTF_8)).use { writer ->
writer.write(json)
}
}
val uri = androidx.core.content.FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val intent = android.content.Intent(android.content.Intent.ACTION_SEND).apply {
type = MIME_TYPE
putExtra(android.content.Intent.EXTRA_STREAM, uri)
putExtra(android.content.Intent.EXTRA_SUBJECT,
"Dolby Preset: ${preset.name} (${preset.bandMode.displayName})")
putExtra(android.content.Intent.EXTRA_TEXT,
"Check out this custom Dolby ${preset.bandMode.displayName} audio preset!")
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
Result.success(android.content.Intent.createChooser(intent, "Share Preset"))
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun copyPresetToClipboard(preset: EqualizerPreset): Result<Unit> =
withContext(Dispatchers.IO) {
try {
val json = exportPresetToJson(preset)
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
as android.content.ClipboardManager
val clip = android.content.ClipData.newPlainText(
"Dolby Preset: ${preset.name} (${preset.bandMode.displayName})",
json
)
clipboard.setPrimaryClip(clip)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun importPresetFromClipboard(): Result<EqualizerPreset> =
withContext(Dispatchers.IO) {
try {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
as android.content.ClipboardManager
val clip = clipboard.primaryClip
if (clip != null && clip.itemCount > 0) {
val text = clip.getItemAt(0).text.toString()
val preset = importPresetFromJson(text)
Result.success(preset)
} else {
Result.failure(IllegalStateException("No data in clipboard"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.domain.models
import org.lunaris.dolby.data.AppInfo
sealed class AppProfileUiState {
object Loading : AppProfileUiState()
data class Success(
val apps: List<AppInfo>,
val appsWithProfiles: Map<String, Int>
) : AppProfileUiState()
data class Error(val message: String) : AppProfileUiState()
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.domain.models
enum class BandMode(val value: String, val displayName: String, val bandCount: Int) {
TEN_BAND("10", "10 Bands", 10),
FIFTEEN_BAND("15", "15 Bands", 15),
TWENTY_BAND("20", "20 Bands", 20);
companion object {
fun fromValue(value: String): BandMode {
return values().find { it.value == value } ?: TEN_BAND
}
}
}
data class DolbyProfile(
val id: Int,
val nameResId: Int
)
data class DolbySettings(
val enabled: Boolean = true,
val currentProfile: Int = 0,
val bassEnhancerEnabled: Boolean = false,
val volumeLevelerEnabled: Boolean = false,
val bandMode: BandMode = BandMode.TEN_BAND
)
data class ProfileSettings(
val profile: Int,
val ieqPreset: Int = 0,
val headphoneVirtualizerEnabled: Boolean = false,
val speakerVirtualizerEnabled: Boolean = false,
val stereoWideningAmount: Int = 32,
val dialogueEnhancerEnabled: Boolean = false,
val dialogueEnhancerAmount: Int = 6,
val bassLevel: Int = 0,
val trebleLevel: Int = 0,
val bassCurve: Int = 0
)
data class EqualizerPreset(
val name: String,
val bandGains: List<BandGain>,
val isUserDefined: Boolean = false,
val isCustom: Boolean = false,
val bandMode: BandMode = BandMode.TEN_BAND
)
data class BandGain(
val frequency: Int,
val gain: Int = 0
)
sealed class DolbyUiState {
object Loading : DolbyUiState()
data class Success(
val settings: DolbySettings,
val profileSettings: ProfileSettings,
val currentPresetName: String,
val isOnSpeaker: Boolean
) : DolbyUiState()
data class Error(val message: String) : DolbyUiState()
}
sealed class EqualizerUiState {
object Loading : EqualizerUiState()
data class Success(
val presets: List<EqualizerPreset>,
val currentPreset: EqualizerPreset,
val bandGains: List<BandGain>,
val bandMode: BandMode
) : EqualizerUiState()
data class Error(val message: String) : EqualizerUiState()
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* (C) 2023-24 Paranoid Android
* (C) 2024-2025 Lunaris AOSP
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.provider
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import org.lunaris.dolby.R
import org.lunaris.dolby.data.DolbyRepository
private const val KEY_DOLBY = "dolby"
private const val META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary"
class SummaryProvider : ContentProvider() {
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
val summary = when (method) {
KEY_DOLBY -> getDolbySummary()
else -> return null
}
return Bundle().apply {
putString(META_DATA_PREFERENCE_SUMMARY, summary)
}
}
override fun onCreate(): Boolean = true
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int = 0
private fun getDolbySummary(): String {
val context = context ?: return ""
val repository = DolbyRepository(context)
if (!repository.getDolbyEnabled()) {
return context.getString(R.string.dolby_off)
}
val profile = repository.getCurrentProfile()
val profiles = context.resources.getStringArray(R.array.dolby_profile_entries)
val profileValues = context.resources.getStringArray(R.array.dolby_profile_values)
return try {
val index = profileValues.indexOf(profile.toString())
val profileName = if (index != -1) profiles[index] else context.getString(R.string.dolby_unknown)
context.getString(R.string.dolby_on_with_profile, profileName)
} catch (e: Exception) {
context.getString(R.string.dolby_on)
}
}
}

View File

@@ -0,0 +1,288 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.service
import android.app.Service
import android.app.usage.UsageEvents
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.Intent
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.R
import org.lunaris.dolby.data.AppProfileManager
import org.lunaris.dolby.data.DolbyRepository
import org.lunaris.dolby.utils.ToastHelper
import java.util.concurrent.atomic.AtomicReference
class AppProfileMonitorService : Service() {
private val handler = Handler(Looper.getMainLooper())
private val switchHandler = Handler(Looper.getMainLooper())
private lateinit var appProfileManager: AppProfileManager
private lateinit var dolbyRepository: DolbyRepository
private lateinit var audioManager: AudioManager
private val lastPackageName = AtomicReference<String?>(null)
private var originalProfile: Int = -1
private var isMonitoring = false
private var pendingSwitchRunnable: Runnable? = null
private var hasOriginalProfile = false
private var lastProfileChangeTime: Long = 0
private val checkForegroundAppRunnable = object : Runnable {
override fun run() {
checkForegroundApp()
if (isMonitoring) {
handler.postDelayed(this, CHECK_INTERVAL)
}
}
}
override fun onCreate() {
super.onCreate()
appProfileManager = AppProfileManager(this)
dolbyRepository = DolbyRepository(this)
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val prefs = getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
val savedProfile = prefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull() ?: 0
if (!hasOriginalProfile) {
originalProfile = savedProfile
hasOriginalProfile = true
DolbyConstants.dlog(TAG, "Service created - saved original profile: $originalProfile")
} else {
DolbyConstants.dlog(TAG, "Service created - keeping existing original profile: $originalProfile")
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START_MONITORING -> startMonitoring()
ACTION_STOP_MONITORING -> stopMonitoring()
}
return START_STICKY
}
private fun startMonitoring() {
if (!isMonitoring) {
isMonitoring = true
if (!hasOriginalProfile) {
val prefs = getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
originalProfile = prefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull() ?: 0
hasOriginalProfile = true
DolbyConstants.dlog(TAG, "Re-initialized original profile on start: $originalProfile")
}
DolbyConstants.dlog(TAG, "Started monitoring foreground app (original profile: $originalProfile)")
handler.post(checkForegroundAppRunnable)
}
}
private fun stopMonitoring() {
if (isMonitoring) {
isMonitoring = false
handler.removeCallbacks(checkForegroundAppRunnable)
synchronized(this) {
pendingSwitchRunnable?.let { switchHandler.removeCallbacks(it) }
pendingSwitchRunnable = null
}
if (hasOriginalProfile && originalProfile >= 0) {
DolbyConstants.dlog(TAG, "Restoring original profile: $originalProfile")
dolbyRepository.setCurrentProfile(originalProfile)
val prefs = getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
val currentProfile = prefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull() ?: 0
if (currentProfile != originalProfile) {
DolbyConstants.dlog(TAG, "WARNING: Profile restoration mismatch! Expected: $originalProfile, Got: $currentProfile")
} else {
DolbyConstants.dlog(TAG, "Profile restored successfully")
}
} else {
DolbyConstants.dlog(TAG, "No valid original profile to restore (hasOriginal=$hasOriginalProfile, profile=$originalProfile)")
}
DolbyConstants.dlog(TAG, "Stopped monitoring foreground app")
}
}
private fun isHeadphoneConnected(): Boolean {
return try {
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
devices.any { device ->
device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
device.type == AudioDeviceInfo.TYPE_USB_HEADSET ||
device.type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
device.type == AudioDeviceInfo.TYPE_BLE_SPEAKER
}
} catch (e: Exception) {
Log.e(TAG, "Error checking headphone connection", e)
false
}
}
private fun checkForegroundApp() {
try {
val prefs = getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
val headphoneOnlyMode = prefs.getBoolean("app_profile_headphone_only", false)
if (headphoneOnlyMode && !isHeadphoneConnected()) {
DolbyConstants.dlog(TAG, "Headphone-only mode enabled but no headphones connected, skipping profile switch")
return
}
val packageName = getForegroundPackage() ?: return
val previousPackage = lastPackageName.getAndSet(packageName)
if (packageName == previousPackage) {
return
}
DolbyConstants.dlog(TAG, "Foreground app changed: $previousPackage -> $packageName")
synchronized(this) {
pendingSwitchRunnable?.let { switchHandler.removeCallbacks(it) }
pendingSwitchRunnable = Runnable {
synchronized(this) {
try {
if (headphoneOnlyMode && !isHeadphoneConnected()) {
DolbyConstants.dlog(TAG, "Headphones disconnected, aborting profile switch")
return@Runnable
}
val assignedProfile = appProfileManager.getAppProfile(packageName)
val showToasts = prefs.getBoolean("app_profile_show_toasts", true)
if (assignedProfile >= 0) {
DolbyConstants.dlog(TAG, "Switching to profile $assignedProfile for $packageName")
lastProfileChangeTime = System.currentTimeMillis()
dolbyRepository.setCurrentProfile(assignedProfile)
DolbyConstants.dlog(TAG, "App profile active - original profile remains: $originalProfile")
if (showToasts) {
val profileName = getProfileName(assignedProfile)
val appName = getAppName(packageName)
ToastHelper.showToast(
this@AppProfileMonitorService,
"Dolby: $profileName ($appName)"
)
}
} else {
if (hasOriginalProfile && originalProfile >= 0) {
val currentProfile = prefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull() ?: 0
if (currentProfile != originalProfile) {
DolbyConstants.dlog(TAG, "Restoring original profile $originalProfile for $packageName (current: $currentProfile)")
lastProfileChangeTime = System.currentTimeMillis()
dolbyRepository.setCurrentProfile(originalProfile)
} else {
DolbyConstants.dlog(TAG, "Already on original profile $originalProfile, no change needed")
}
} else {
DolbyConstants.dlog(TAG, "No original profile to restore (hasOriginal=$hasOriginalProfile, profile=$originalProfile)")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error switching profile", e)
} finally {
pendingSwitchRunnable = null
}
}
}
switchHandler.postDelayed(pendingSwitchRunnable!!, SWITCH_DELAY)
}
} catch (e: Exception) {
Log.e(TAG, "Error checking foreground app", e)
}
}
private fun getForegroundPackage(): String? {
val usageStatsManager = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val currentTime = System.currentTimeMillis()
val usageEvents = usageStatsManager.queryEvents(currentTime - 1000, currentTime)
val event = UsageEvents.Event()
var lastPackage: String? = null
while (usageEvents.hasNextEvent()) {
usageEvents.getNextEvent(event)
if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
lastPackage = event.packageName
}
}
return lastPackage
}
private fun getProfileName(profile: Int): String {
val profiles = resources.getStringArray(R.array.dolby_profile_entries)
val profileValues = resources.getStringArray(R.array.dolby_profile_values)
return try {
val index = profileValues.indexOfFirst { it.toInt() == profile }
if (index >= 0) profiles[index] else "Unknown"
} catch (e: Exception) {
"Unknown"
}
}
private fun getAppName(packageName: String): String {
return try {
val appInfo = packageManager.getApplicationInfo(packageName, 0)
packageManager.getApplicationLabel(appInfo).toString()
} catch (e: Exception) {
packageName
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
super.onDestroy()
DolbyConstants.dlog(TAG, "Service destroyed")
stopMonitoring()
dolbyRepository.close()
hasOriginalProfile = false
}
companion object {
private const val TAG = "AppProfileMonitor"
private const val CHECK_INTERVAL = 2000L
private const val SWITCH_DELAY = 300L
const val ACTION_START_MONITORING = "org.lunaris.dolby.START_MONITORING"
const val ACTION_STOP_MONITORING = "org.lunaris.dolby.STOP_MONITORING"
fun startMonitoring(context: Context) {
val intent = Intent(context, AppProfileMonitorService::class.java).apply {
action = ACTION_START_MONITORING
}
context.startService(intent)
}
fun stopMonitoring(context: Context) {
val intent = Intent(context, AppProfileMonitorService::class.java).apply {
action = ACTION_STOP_MONITORING
}
context.startService(intent)
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.service
import android.content.Intent
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.util.Log
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.data.AppProfileManager
import org.lunaris.dolby.data.DolbyRepository
class DolbyNotificationListener : NotificationListenerService() {
private lateinit var appProfileManager: AppProfileManager
private lateinit var dolbyRepository: DolbyRepository
private var lastActivePackage: String? = null
override fun onCreate() {
super.onCreate()
DolbyConstants.dlog(TAG, "NotificationListener created")
appProfileManager = AppProfileManager(this)
dolbyRepository = DolbyRepository(this)
initializeDolbySettings()
startAppProfileMonitoringIfEnabled()
}
override fun onListenerConnected() {
super.onListenerConnected()
DolbyConstants.dlog(TAG, "NotificationListener connected")
initializeDolbySettings()
startAppProfileMonitoringIfEnabled()
}
override fun onListenerDisconnected() {
super.onListenerDisconnected()
DolbyConstants.dlog(TAG, "NotificationListener disconnected")
requestRebind(android.content.ComponentName(this, DolbyNotificationListener::class.java))
}
override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?.packageName?.let { packageName ->
if (packageName != lastActivePackage && packageName != this.packageName) {
lastActivePackage = packageName
handlePackageChange(packageName)
}
}
}
override fun onNotificationRemoved(sbn: StatusBarNotification?) {
super.onNotificationRemoved(sbn)
}
private fun initializeDolbySettings() {
try {
val prefs = getSharedPreferences("dolby_prefs", MODE_PRIVATE)
val savedProfile = prefs.getString(DolbyConstants.PREF_PROFILE, "0")?.toIntOrNull() ?: 0
val enabled = prefs.getBoolean(DolbyConstants.PREF_ENABLE, false)
DolbyConstants.dlog(TAG, "Initializing Dolby - enabled: $enabled, profile: $savedProfile")
if (enabled) {
dolbyRepository.setCurrentProfile(savedProfile)
dolbyRepository.setDolbyEnabled(true)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize Dolby settings", e)
}
}
private fun startAppProfileMonitoringIfEnabled() {
val prefs = getSharedPreferences("dolby_prefs", MODE_PRIVATE)
val isEnabled = prefs.getBoolean("app_profile_monitoring_enabled", false)
if (isEnabled) {
DolbyConstants.dlog(TAG, "Starting app profile monitoring")
AppProfileMonitorService.startMonitoring(this)
}
}
private fun handlePackageChange(packageName: String) {
val prefs = getSharedPreferences("dolby_prefs", MODE_PRIVATE)
val isMonitoringEnabled = prefs.getBoolean("app_profile_monitoring_enabled", false)
if (!isMonitoringEnabled) return
try {
val assignedProfile = appProfileManager.getAppProfile(packageName)
if (assignedProfile >= 0) {
DolbyConstants.dlog(TAG, "Package change detected: $packageName -> profile $assignedProfile")
}
} catch (e: Exception) {
Log.e(TAG, "Error handling package change", e)
}
}
override fun onDestroy() {
super.onDestroy()
DolbyConstants.dlog(TAG, "NotificationListener destroyed")
}
companion object {
private const val TAG = "DolbyNotificationListener"
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.tile
import android.content.res.Resources
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import org.lunaris.dolby.R
import org.lunaris.dolby.data.DolbyRepository
class DolbyTileService : TileService() {
private val repository by lazy { DolbyRepository(applicationContext) }
override fun onStartListening() {
super.onStartListening()
updateTile()
}
override fun onClick() {
super.onClick()
val enabled = repository.getDolbyEnabled()
repository.setDolbyEnabled(!enabled)
updateTile()
}
private fun updateTile() {
qsTile?.apply {
val enabled = repository.getDolbyEnabled()
state = if (enabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
subtitle = getProfileName()
updateTile()
}
}
private fun getProfileName(): String {
val profile = repository.getCurrentProfile()
val profiles = resources.getStringArray(R.array.dolby_profile_entries)
val profileValues = resources.getStringArray(R.array.dolby_profile_values)
return try {
val index = profileValues.indexOf(profile.toString())
if (index != -1) profiles[index] else getString(R.string.dolby_unknown)
} catch (e: Exception) {
getString(R.string.dolby_unknown)
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.ui.screens.DolbyNavHost
import org.lunaris.dolby.ui.theme.DolbyTheme
import org.lunaris.dolby.ui.viewmodel.DolbyViewModel
import org.lunaris.dolby.ui.viewmodel.EqualizerViewModel
class DolbyActivity : ComponentActivity() {
private val dolbyViewModel: DolbyViewModel by viewModels()
private val equalizerViewModel: EqualizerViewModel by viewModels()
private val audioManager by lazy { getSystemService(AudioManager::class.java) }
private val handler = Handler(Looper.getMainLooper())
private var isAudioCallbackRegistered = false
private var isActivityActive = false
private val audioDeviceCallback = object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
if (isActivityActive) {
DolbyConstants.dlog(TAG, "Audio device added")
handler.post {
dolbyViewModel.updateSpeakerState()
}
}
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
if (isActivityActive) {
DolbyConstants.dlog(TAG, "Audio device removed")
handler.post {
dolbyViewModel.updateSpeakerState()
}
}
}
}
private val lifecycleObserver = object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
DolbyConstants.dlog(TAG, "Lifecycle: onCreate")
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
DolbyConstants.dlog(TAG, "Lifecycle: onStart")
isActivityActive = true
registerAudioCallback()
dolbyViewModel.loadSettings()
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
DolbyConstants.dlog(TAG, "Lifecycle: onResume")
dolbyViewModel.updateSpeakerState()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
DolbyConstants.dlog(TAG, "Lifecycle: onPause")
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
DolbyConstants.dlog(TAG, "Lifecycle: onStop")
isActivityActive = false
if (!isChangingConfigurations) {
unregisterAudioCallback()
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
DolbyConstants.dlog(TAG, "Lifecycle: onDestroy")
cleanupResources()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DolbyConstants.dlog(TAG, "Activity onCreate")
lifecycle.addObserver(lifecycleObserver)
setContent {
DolbyTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surface
) {
DolbyNavHost(
dolbyViewModel = dolbyViewModel,
equalizerViewModel = equalizerViewModel
)
}
}
}
}
private fun registerAudioCallback() {
if (!isAudioCallbackRegistered) {
try {
audioManager.registerAudioDeviceCallback(audioDeviceCallback, handler)
isAudioCallbackRegistered = true
DolbyConstants.dlog(TAG, "Audio callback registered")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Failed to register audio callback: ${e.message}")
}
}
}
private fun unregisterAudioCallback() {
if (isAudioCallbackRegistered) {
try {
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
isAudioCallbackRegistered = false
DolbyConstants.dlog(TAG, "Audio callback unregistered")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Failed to unregister audio callback: ${e.message}")
}
}
}
private fun cleanupResources() {
try {
unregisterAudioCallback()
handler.removeCallbacksAndMessages(null)
lifecycle.removeObserver(lifecycleObserver)
DolbyConstants.dlog(TAG, "Resources cleaned up successfully")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error during cleanup: ${e.message}")
}
}
override fun onDestroy() {
DolbyConstants.dlog(TAG, "Activity onDestroy")
super.onDestroy()
}
companion object {
private const val TAG = "DolbyActivity"
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.pow
@Composable
fun AnimatedEqualizerIconDynamic(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.onPrimaryContainer,
size: Dp = 24.dp,
barCount: Int = 5
) {
val infiniteTransition = rememberInfiniteTransition(label = "equalizer_dynamic")
val barHeights = List(barCount) { index ->
infiniteTransition.animateFloat(
initialValue = 0.2f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 800 + (index * 50),
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
),
label = "bar_height_$index"
)
}
Canvas(modifier = modifier.size(size)) {
val canvasWidth = this.size.width
val canvasHeight = this.size.height
val barWidths = List(barCount) { index ->
val normalizedPosition = index.toFloat() / (barCount - 1)
val centerOffset = (normalizedPosition - 0.5f) * 2
val widthFactor = 1.0f - (centerOffset * centerOffset).pow(0.6f)
val scaledWidth = 0.5f + (widthFactor * 0.5f)
scaledWidth
}
val totalWidthFactor = barWidths.sum() + (barCount - 1) * 0.3f
val baseBarWidth = canvasWidth / totalWidthFactor
var currentX = 0f
barHeights.forEachIndexed { index, heightAnimation ->
val barWidth = baseBarWidth * barWidths[index]
val barHeight = canvasHeight * heightAnimation.value
val y = canvasHeight - barHeight
drawRoundRect(
color = color,
topLeft = Offset(currentX, y),
size = Size(barWidth, barHeight),
cornerRadius = CornerRadius(barWidth / 2, barWidth / 2)
)
currentX += barWidth + (baseBarWidth * 0.3f)
}
}
}
@Composable
fun AnimatedEqualizerHeader(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.onPrimaryContainer,
width: Dp = 120.dp,
height: Dp = 56.dp,
barCount: Int = 9
) {
val infiniteTransition = rememberInfiniteTransition(label = "equalizer_header")
val barHeights = List(barCount) { index ->
val normalizedPosition = index.toFloat() / (barCount - 1)
val centerOffset = kotlin.math.abs((normalizedPosition - 0.5f) * 2)
val heightScale = 1.0f - (centerOffset * centerOffset * 0.6f)
infiniteTransition.animateFloat(
initialValue = 0.25f * heightScale,
targetValue = 0.95f * heightScale,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 900 + (index * 60),
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
),
label = "bar_height_$index"
)
}
Canvas(modifier = modifier.size(width = width, height = height)) {
val canvasWidth = this.size.width
val canvasHeight = this.size.height
val barWidths = List(barCount) { index ->
val normalizedPosition = index.toFloat() / (barCount - 1)
val centerOffset = (normalizedPosition - 0.5f) * 2
val widthFactor = 1.0f - (centerOffset * centerOffset).pow(0.7f)
val scaledWidth = 0.45f + (widthFactor * 0.55f)
scaledWidth
}
val totalWidthFactor = barWidths.sum() + (barCount - 1) * 0.25f
val baseBarWidth = canvasWidth / totalWidthFactor
var currentX = 0f
barHeights.forEachIndexed { index, heightAnimation ->
val barWidth = baseBarWidth * barWidths[index]
val barHeight = canvasHeight * heightAnimation.value
val y = canvasHeight - barHeight
drawRoundRect(
color = color.copy(alpha = 0.85f),
topLeft = Offset(currentX, y),
size = Size(barWidth, barHeight),
cornerRadius = CornerRadius(barWidth / 2, barWidth / 2)
)
currentX += barWidth + (baseBarWidth * 0.25f)
}
}
}

View File

@@ -0,0 +1,326 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import android.content.Context
import android.content.Intent
import android.provider.Settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.lunaris.dolby.R
import org.lunaris.dolby.service.AppProfileMonitorService
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppProfileSettingsCard(
onManageClick: () -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("dolby_prefs", Context.MODE_PRIVATE)
var isEnabled by remember {
mutableStateOf(prefs.getBoolean("app_profile_monitoring_enabled", false))
}
var showToasts by remember {
mutableStateOf(prefs.getBoolean("app_profile_show_toasts", true))
}
var headphoneOnlyMode by remember {
mutableStateOf(prefs.getBoolean("app_profile_headphone_only", false))
}
var showPermissionDialog by remember { mutableStateOf(false) }
Card(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceBright
)
) {
Column(modifier = Modifier.padding(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 16.dp)
) {
Surface(
modifier = Modifier.size(40.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.Apps,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.app_profiles_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
Text(
text = stringResource(R.string.app_profiles_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 16.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.app_profiles_auto_switch),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Switch(
checked = isEnabled,
onCheckedChange = { enabled ->
if (enabled && !hasUsageStatsPermission(context)) {
showPermissionDialog = true
} else {
isEnabled = enabled
prefs.edit().putBoolean("app_profile_monitoring_enabled", enabled).apply()
if (enabled) {
AppProfileMonitorService.startMonitoring(context)
} else {
AppProfileMonitorService.stopMonitoring(context)
}
}
},
thumbContent = {
Crossfade(
targetState = isEnabled,
animationSpec = MaterialTheme.motionScheme.slowEffectsSpec(),
label = "switch_icon"
) { isChecked ->
if (isChecked) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
)
}
AnimatedVisibility(visible = isEnabled) {
Column {
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = context.getString(R.string.app_profiles_headphone_only),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = context.getString(R.string.app_profiles_headphone_only_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))
Switch(
checked = headphoneOnlyMode,
onCheckedChange = { enabled ->
headphoneOnlyMode = enabled
prefs.edit().putBoolean("app_profile_headphone_only", enabled).apply()
},
thumbContent = {
Crossfade(
targetState = headphoneOnlyMode,
animationSpec = MaterialTheme.motionScheme.slowEffectsSpec(),
label = "headphone_switch_icon"
) { isChecked ->
if (isChecked) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.app_profiles_show_toasts),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Switch(
checked = showToasts,
onCheckedChange = { show ->
showToasts = show
prefs.edit().putBoolean("app_profile_show_toasts", show).apply()
},
thumbContent = {
Crossfade(
targetState = showToasts,
animationSpec = MaterialTheme.motionScheme.slowEffectsSpec(),
label = "toast_switch_icon"
) { isChecked ->
if (isChecked) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = onManageClick,
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Icon(
Icons.Default.Apps,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.app_profiles_manage))
}
}
}
if (showPermissionDialog) {
AlertDialog(
onDismissRequest = { showPermissionDialog = false },
icon = {
Icon(
Icons.Default.Security,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
stringResource(R.string.app_profiles_permission_required),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Text(
stringResource(R.string.app_profiles_permission_required_details),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
confirmButton = {
Button(
onClick = {
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
context.startActivity(intent)
showPermissionDialog = false
},
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.app_profiles_grant_permission))
}
},
dismissButton = {
TextButton(
onClick = { showPermissionDialog = false },
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.cancel))
}
},
shape = MaterialTheme.shapes.extraLarge
)
}
}
private fun hasUsageStatsPermission(context: Context): Boolean {
val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as android.app.AppOpsManager
val mode = appOpsManager.checkOpNoThrow(
"android:get_usage_stats",
android.os.Process.myUid(),
context.packageName
)
return mode == android.app.AppOpsManager.MODE_ALLOWED
}

View File

@@ -0,0 +1,386 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
data class Contributor(
val name: String,
val githubUsername: String,
val contribution: String,
val isHighlighted: Boolean = false
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun CreditsDialog(
onDismiss: () -> Unit
) {
val context = LocalContext.current
val repoUrl = "https://github.com/Pong-Development/hardware_dolby"
val mainContributors = listOf(
Contributor(
name = "Ghost",
githubUsername = "Ghosuto",
contribution = "Rewrite Dolby in Compose (Lunaris Dolby)",
isHighlighted = true
),
Contributor(
name = "Adithya R",
githubUsername = "adithya2306",
contribution = "AOSPA Dolby Manager (Initial Code)",
isHighlighted = true
),
Contributor(
name = "Kenway",
githubUsername = "kenway214",
contribution = "Base & Treble Changes, EQ Tuning",
isHighlighted = true
)
)
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
modifier = Modifier
.fillMaxWidth(0.95f)
.fillMaxHeight(0.85f),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(140.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f),
MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.8f)
)
)
),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Credits & Contributors",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentPadding = PaddingValues(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Thank you to all contributors who made this project possible!",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
item {
Card(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.clickable {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(repoUrl))
context.startActivity(intent)
},
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
modifier = Modifier.size(40.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primary
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.Code,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "View on GitHub",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "hardware_dolby",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
)
}
Icon(
imageVector = Icons.Default.OpenInNew,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(20.dp)
)
}
}
}
item {
Text(
text = "Main Contributors",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
)
}
items(mainContributors) { contributor ->
ContributorCard(contributor = contributor)
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.People,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "And all other contributors",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Check the GitHub repository for the complete list",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
textAlign = TextAlign.Center
)
}
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth(0.5f),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Close")
}
}
}
}
}
}
@Composable
private fun ContributorCard(
contributor: Contributor
) {
val context = LocalContext.current
val githubUrl = "https://github.com/${contributor.githubUsername}"
Card(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.clickable {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubUrl))
context.startActivity(intent)
},
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = if (contributor.isHighlighted)
MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
modifier = Modifier.size(48.dp),
shape = MaterialTheme.shapes.medium,
color = if (contributor.isHighlighted)
MaterialTheme.colorScheme.tertiary
else
MaterialTheme.colorScheme.primary
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
tint = if (contributor.isHighlighted)
MaterialTheme.colorScheme.onTertiary
else
MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(28.dp)
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = contributor.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(2.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Code,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "@${contributor.githubUsername}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = contributor.contribution,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.Default.OpenInNew,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import org.lunaris.dolby.R
import org.lunaris.dolby.utils.*
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun EnhancedBottomNavigationBar(
currentRoute: String,
onNavigate: (String) -> Unit
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 3.dp,
shadowElevation = 8.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 12.dp)
.windowInsetsPadding(WindowInsets.navigationBars),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
val isHomeSelected = currentRoute == "settings"
val isEqualizerSelected = currentRoute == "equalizer"
val isAdvancedSelected = currentRoute == "advanced"
EnhancedNavItem(
icon = Icons.Default.Home,
label = stringResource(R.string.home),
selected = isHomeSelected,
onClick = { onNavigate("settings") },
isMiddleItem = false,
isSiblingSelected = isAdvancedSelected,
modifier = Modifier.weight(1f)
)
EnhancedNavItem(
icon = Icons.Default.GraphicEq,
label = stringResource(R.string.equalizer),
selected = isEqualizerSelected,
onClick = { onNavigate("equalizer") },
isEqualizer = true,
isMiddleItem = true,
isSiblingSelected = isHomeSelected || isAdvancedSelected,
modifier = Modifier.weight(1f)
)
EnhancedNavItem(
icon = Icons.Default.Settings,
label = stringResource(R.string.advanced),
selected = isAdvancedSelected,
onClick = { onNavigate("advanced") },
isMiddleItem = false,
isSiblingSelected = isHomeSelected,
modifier = Modifier.weight(1f)
)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun EnhancedNavItem(
icon: ImageVector,
label: String,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEqualizer: Boolean = false,
isMiddleItem: Boolean = false,
isSiblingSelected: Boolean = false
) {
val interactionSource = remember { MutableInteractionSource() }
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
var isBouncing by remember { mutableStateOf(false) }
val bounceScale by animateFloatAsState(
targetValue = if (isBouncing) 1.3f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
),
finishedListener = { isBouncing = false },
label = "bounce_scale"
)
val backgroundWidth by animateDpAsState(
targetValue = if (selected) 120.dp else 52.dp,
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
label = "background_width"
)
val backgroundHeight by animateDpAsState(
targetValue = if (selected) 52.dp else 48.dp,
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
label = "background_height"
)
val horizontalPadding by animateDpAsState(
targetValue = when {
isMiddleItem && isSiblingSelected -> 4.dp
else -> 8.dp
},
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
label = "horizontal_padding"
)
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.height(backgroundHeight)
.width(backgroundWidth)
.clip(RoundedCornerShape(if (selected) 50.dp else 16.dp))
.background(
color = if (selected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceContainerHighest
}
)
.clickable(
interactionSource = interactionSource,
indication = null
) {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.HEAVY_CLICK)
}
isBouncing = true
onClick()
},
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier.scale(bounceScale),
contentAlignment = Alignment.Center
) {
if (isEqualizer) {
AnimatedEqualizerIconDynamic(
color = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
size = 24.dp
)
} else {
Icon(
imageVector = icon,
contentDescription = label,
tint = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.size(24.dp)
)
}
}
AnimatedVisibility(
visible = selected,
enter = fadeIn(
animationSpec = tween(300, delayMillis = 100)
) + expandHorizontally(
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
expandFrom = Alignment.Start
),
exit = fadeOut(
animationSpec = tween(200)
) + shrinkHorizontally(
animationSpec = tween(300),
shrinkTowards = Alignment.Start
)
) {
Row {
Spacer(modifier = Modifier.width(6.dp))
Text(
text = label,
style = MaterialTheme.typography.labelLarge.copy(
fontSize = 13.sp
),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer,
maxLines = 1
)
}
}
}
}
}
}

View File

@@ -0,0 +1,371 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.lunaris.dolby.domain.models.BandGain
import kotlin.math.abs
@Composable
fun InteractiveFrequencyResponseCurve(
bandGains: List<BandGain>,
onBandGainChange: (index: Int, newGain: Int) -> Unit,
modifier: Modifier = Modifier,
isActive: Boolean = false,
isEditable: Boolean = true
) {
val primaryColor = MaterialTheme.colorScheme.primary
val surfaceColor = MaterialTheme.colorScheme.surfaceVariant
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
val secondaryColor = MaterialTheme.colorScheme.secondary
val primaryContainerColor = MaterialTheme.colorScheme.primaryContainer
val errorColor = MaterialTheme.colorScheme.error
val surfaceContainerHighest = MaterialTheme.colorScheme.surfaceContainerHighest
val backgroundColor = if (isActive) {
primaryContainerColor.copy(alpha = 0.4f)
} else {
surfaceContainerHighest.copy(alpha = 0.3f)
}
val borderColor = if (isActive) {
primaryColor
} else if (!isEditable) {
errorColor.copy(alpha = 0.5f)
} else {
Color.Transparent
}
val borderWidth = if (isActive) 2.dp else if (!isEditable) 1.dp else 0.dp
var draggedIndex by remember { mutableStateOf<Int?>(null) }
var controlPoints by remember { mutableStateOf(bandGains.map { it.gain }) }
LaunchedEffect(bandGains) {
if (draggedIndex == null) {
controlPoints = bandGains.map { it.gain }
}
}
Box(modifier = modifier) {
Canvas(
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large)
.background(backgroundColor)
.border(
width = borderWidth,
color = borderColor,
shape = MaterialTheme.shapes.large
)
.pointerInput(isEditable) {
if (isEditable) {
detectDragGestures(
onDragStart = { offset ->
val width = size.width
val height = size.height
val stepX = width / (bandGains.size - 1).toFloat()
var closestIndex = -1
var closestDistance = Float.MAX_VALUE
bandGains.forEachIndexed { index, _ ->
val x = index * stepX
val normalizedGain = (controlPoints[index] / 150f).coerceIn(-1f, 1f)
val y = height / 2 - (normalizedGain * height / 2 * 0.85f)
val distance = kotlin.math.sqrt(
(offset.x - x) * (offset.x - x) +
(offset.y - y) * (offset.y - y)
)
if (distance < closestDistance && distance < 120f) {
closestDistance = distance
closestIndex = index
}
}
if (closestIndex != -1) {
draggedIndex = closestIndex
}
},
onDrag = { change, _ ->
draggedIndex?.let { index ->
val height = size.height
val centerY = height / 2
val y = change.position.y
val normalizedGain = ((centerY - y) / (height / 2 * 0.85f)).coerceIn(-1f, 1f)
val newGain = (normalizedGain * 150).toInt().coerceIn(-150, 150)
if (controlPoints[index] != newGain) {
controlPoints = controlPoints.toMutableList().apply {
this[index] = newGain
}
}
change.consume()
}
},
onDragEnd = {
draggedIndex?.let { index ->
onBandGainChange(index, controlPoints[index])
}
draggedIndex = null
},
onDragCancel = {
draggedIndex = null
}
)
}
}
) {
val width = size.width
val height = size.height
val centerY = height / 2
val gridColor = if (isActive) {
surfaceColor.copy(alpha = 0.4f)
} else {
surfaceColor.copy(alpha = 0.3f)
}
val gridVerticalColor = if (isActive) {
surfaceColor.copy(alpha = 0.3f)
} else {
surfaceColor.copy(alpha = 0.2f)
}
drawLine(
color = if (isActive) {
surfaceColor.copy(alpha = 0.7f)
} else {
surfaceColor.copy(alpha = 0.5f)
},
start = Offset(0f, centerY),
end = Offset(width, centerY),
strokeWidth = 3f
)
for (i in 1..4) {
val y = (height / 5) * i
drawLine(
color = gridColor.copy(alpha = 0.3f),
start = Offset(0f, y),
end = Offset(width, y),
strokeWidth = 1f
)
}
val stepX = width / (bandGains.size - 1)
bandGains.forEachIndexed { index, _ ->
val x = index * stepX
drawLine(
color = gridVerticalColor,
start = Offset(x, 0f),
end = Offset(x, height),
strokeWidth = 1f
)
}
if (bandGains.isNotEmpty() && controlPoints.isNotEmpty()) {
val path = Path()
controlPoints.forEachIndexed { index, gain ->
val x = index * stepX
val normalizedGain = (gain / 150f).coerceIn(-1f, 1f)
val y = centerY - (normalizedGain * centerY * 0.85f)
if (index == 0) {
path.moveTo(x, y)
} else {
val prevX = (index - 1) * stepX
val prevGain = controlPoints[index - 1]
val prevNormalizedGain = (prevGain / 150f).coerceIn(-1f, 1f)
val prevY = centerY - (prevNormalizedGain * centerY * 0.85f)
val cpX1 = prevX + stepX * 0.4f
val cpY1 = prevY
val cpX2 = x - stepX * 0.4f
val cpY2 = y
path.cubicTo(cpX1, cpY1, cpX2, cpY2, x, y)
}
}
drawPath(
path = path,
color = if (isActive) primaryColor else primaryColor.copy(alpha = 0.8f),
style = Stroke(width = if (isActive) 5f else 4f)
)
val fillPath = Path().apply {
addPath(path)
lineTo(width, height)
lineTo(0f, height)
close()
}
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
colors = if (isActive) {
listOf(
primaryColor.copy(alpha = 0.4f),
primaryColor.copy(alpha = 0.08f)
)
} else {
listOf(
primaryColor.copy(alpha = 0.3f),
primaryColor.copy(alpha = 0.05f)
)
}
)
)
controlPoints.forEachIndexed { index, gain ->
val x = index * stepX
val normalizedGain = (gain / 150f).coerceIn(-1f, 1f)
val y = centerY - (normalizedGain * centerY * 0.85f)
val isBeingDragged = draggedIndex == index
val pointRadius = if (isBeingDragged) 14f else 10f
if (isBeingDragged) {
drawCircle(
color = primaryColor.copy(alpha = 0.3f),
radius = 24f,
center = Offset(x, y)
)
}
drawCircle(
color = Color.White,
radius = pointRadius,
center = Offset(x, y)
)
drawCircle(
color = if (isActive) primaryColor else primaryColor.copy(alpha = 0.8f),
radius = pointRadius - 2f,
center = Offset(x, y)
)
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(bottom = 8.dp, start = 8.dp, end = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
bandGains.forEach { bandGain ->
Text(
text = if (bandGain.frequency >= 1000) {
"${bandGain.frequency / 1000}k"
} else {
"${bandGain.frequency}"
},
style = MaterialTheme.typography.labelSmall,
color = if (isActive) {
onSurfaceColor.copy(alpha = 0.8f)
} else {
onSurfaceColor.copy(alpha = 0.7f)
},
fontSize = 10.sp,
fontWeight = FontWeight.Medium
)
}
}
Column(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 4.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "+15",
style = MaterialTheme.typography.labelSmall,
color = if (isActive) {
secondaryColor
} else {
secondaryColor.copy(alpha = 0.8f)
},
fontSize = 10.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(vertical = 12.dp)
)
Text(
text = "0",
style = MaterialTheme.typography.labelSmall,
color = if (isActive) {
secondaryColor
} else {
secondaryColor.copy(alpha = 0.8f)
},
fontSize = 10.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(vertical = 12.dp)
)
Text(
text = "-15",
style = MaterialTheme.typography.labelSmall,
color = if (isActive) {
secondaryColor
} else {
secondaryColor.copy(alpha = 0.8f)
},
fontSize = 10.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(vertical = 12.dp)
)
}
draggedIndex?.let { index ->
val gain = controlPoints[index]
val gainDb = gain / 10f
Surface(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 8.dp),
shape = MaterialTheme.shapes.small,
color = primaryColor,
shadowElevation = 4.dp
) {
Text(
text = "${if (gainDb >= 0) "+" else ""}%.1f dB".format(gainDb),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}
}

View File

@@ -0,0 +1,711 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.indication
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.lunaris.dolby.R
import org.lunaris.dolby.domain.models.ProfileSettings
import org.lunaris.dolby.ui.viewmodel.DolbyViewModel
import org.lunaris.dolby.utils.*
@Composable
fun Modifier.squishable(
enabled: Boolean = true,
scaleDown: Float = 0.93f
): Modifier {
var isPressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (isPressed && enabled) scaleDown else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
),
label = "squish_scale"
)
return this
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.indication(
interactionSource = remember { MutableInteractionSource() },
indication = null
)
.pointerInput(enabled) {
if (enabled) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Press -> {
isPressed = true
}
PointerEventType.Release -> {
isPressed = false
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DolbyMainCard(
enabled: Boolean,
onEnabledChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
Card(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceBright
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f),
MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.6f)
)
)
),
contentAlignment = Alignment.Center
) {
AnimatedEqualizerHeader(
color = MaterialTheme.colorScheme.onPrimaryContainer,
width = 140.dp,
height = 64.dp,
barCount = 11
)
}
Column(
modifier = Modifier.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.dolby_enable),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (enabled) stringResource(R.string.dolby_on)
else stringResource(R.string.dolby_off),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = enabled,
onCheckedChange = {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.HEAVY_CLICK)
}
onEnabledChange(it)
},
thumbContent = {
Crossfade(
targetState = enabled,
animationSpec = MaterialTheme.motionScheme.slowEffectsSpec(),
label = "switch_icon"
) { isChecked ->
if (isChecked) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModernProfileSelector(
currentProfile: Int,
onProfileChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
ProfileCarousel(
currentProfile = currentProfile,
onProfileChange = onProfileChange,
modifier = modifier
)
}
@Composable
fun ModernSettingsCard(
title: String,
icon: ImageVector,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceBright
)
) {
Column(modifier = Modifier.padding(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 16.dp)
) {
Surface(
modifier = Modifier.size(40.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
content()
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ModernSettingSwitch(
title: String,
subtitle: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector? = null
) {
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
Surface(
modifier = modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large),
color = if (checked)
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
icon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = if (checked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(12.dp))
}
Column {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Switch(
checked = checked,
onCheckedChange = {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.DOUBLE_CLICK)
}
onCheckedChange(it)
},
thumbContent = {
Crossfade(
targetState = checked,
animationSpec = MaterialTheme.motionScheme.slowEffectsSpec(),
label = "switch_icon"
) { isChecked ->
if (isChecked) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
)
}
}
}
@Composable
fun ModernSettingSlider(
title: String,
value: Int,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int,
modifier: Modifier = Modifier,
valueLabel: (Int) -> String = { it.toString() }
) {
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
var lastHapticValue by remember { mutableIntStateOf(value) }
Column(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.primaryContainer
) {
Text(
text = valueLabel(value),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Slider(
value = value.toFloat(),
onValueChange = { newValue ->
val intValue = newValue.toInt()
if (intValue != lastHapticValue) {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.TEXTURE_TICK)
}
lastHapticValue = intValue
}
onValueChange(newValue)
},
valueRange = valueRange,
steps = steps,
modifier = Modifier.fillMaxWidth(),
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
)
)
}
}
@Composable
fun ModernSettingSelector(
title: String,
currentValue: Int,
entries: Int,
values: Int,
onValueChange: (Int) -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector? = null
) {
val entryList = stringArrayResource(entries)
val valueList = stringArrayResource(values)
val currentIndex = valueList.indexOfFirst { it.toIntOrNull() == currentValue }
val label = entryList.getOrElse(currentIndex.coerceAtLeast(0)) { "" }
var expanded by remember { mutableStateOf(false) }
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
Column(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
icon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
Box {
Surface(
onClick = {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.TICK)
}
expanded = true
},
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
entryList.forEachIndexed { index, entry ->
val value = valueList.getOrNull(index)?.toIntOrNull() ?: return@forEachIndexed
DropdownMenuItem(
text = {
Text(
entry,
color = if (value == currentValue)
MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface
)
},
onClick = {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.CLICK)
}
expanded = false
onValueChange(value)
}
)
}
}
}
}
}
}
@Composable
fun ModernIeqSelector(
currentPreset: Int,
onPresetChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val ieqEntries = stringArrayResource(R.array.dolby_ieq_entries)
val ieqValues = stringArrayResource(R.array.dolby_ieq_values)
val ieqIcons = mapOf(
0 to Icons.Default.PowerOff,
1 to Icons.Default.GraphicEq,
2 to Icons.Rounded.Balance,
3 to Icons.Default.Whatshot
)
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = stringResource(R.string.dolby_ieq),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 12.dp)
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 0 until minOf(2, ieqEntries.size)) {
val entry = ieqEntries[i]
val value = ieqValues[i].toInt()
val isSelected = currentPreset == value
IeqTile(
entry = entry,
value = value,
isSelected = isSelected,
icon = ieqIcons[value] ?: Icons.Default.GraphicEq,
onPresetChange = onPresetChange,
modifier = Modifier.weight(1f)
)
}
}
if (ieqEntries.size > 2) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 2 until minOf(4, ieqEntries.size)) {
val entry = ieqEntries[i]
val value = ieqValues[i].toInt()
val isSelected = currentPreset == value
IeqTile(
entry = entry,
value = value,
isSelected = isSelected,
icon = ieqIcons[value] ?: Icons.Default.GraphicEq,
onPresetChange = onPresetChange,
modifier = Modifier.weight(1f)
)
}
}
}
}
}
}
@Composable
private fun IeqTile(
entry: String,
value: Int,
isSelected: Boolean,
icon: ImageVector,
onPresetChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
Surface(
onClick = {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.DOUBLE_CLICK)
}
onPresetChange(value)
},
modifier = modifier
.height(72.dp)
.squishable(enabled = true, scaleDown = 0.93f),
color = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
shape = if (isSelected)
MaterialTheme.shapes.extraLarge
else
MaterialTheme.shapes.large,
border = if (isSelected)
BorderStroke(2.dp, MaterialTheme.colorScheme.primary)
else null
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Surface(
modifier = Modifier.size(40.dp),
shape = if (isSelected)
MaterialTheme.shapes.extraLarge
else
MaterialTheme.shapes.medium,
color = if (isSelected)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (isSelected)
MaterialTheme.colorScheme.onPrimary
else
MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(22.dp)
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = entry,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium,
color = if (isSelected)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onSurface
)
}
}
}
@Composable
fun ModernConfirmDialog(
title: String,
message: String,
icon: ImageVector = Icons.Default.Info,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Text(
message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
confirmButton = {
Button(
onClick = onConfirm,
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(android.R.string.yes))
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(android.R.string.no))
}
},
shape = MaterialTheme.shapes.extraLarge
)
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.provider.Settings
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.lunaris.dolby.R
import org.lunaris.dolby.service.DolbyNotificationListener
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NotificationListenerPermissionCard(
modifier: Modifier = Modifier
) {
val context = LocalContext.current
var isEnabled by remember { mutableStateOf(isNotificationListenerEnabled(context)) }
var showPermissionDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
isEnabled = isNotificationListenerEnabled(context)
}
AnimatedVisibility(
visible = !isEnabled,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(modifier = Modifier.padding(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 12.dp)
) {
Surface(
modifier = Modifier.size(40.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.error
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = MaterialTheme.colorScheme.onError,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.notification_access_required),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
Text(
text = stringResource(R.string.notification_access_required_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(bottom = 16.dp)
)
Button(
onClick = { showPermissionDialog = true },
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
)
) {
Icon(
Icons.Default.Security,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.enable_notification_access))
}
}
}
}
if (showPermissionDialog) {
AlertDialog(
onDismissRequest = { showPermissionDialog = false },
icon = {
Icon(
Icons.Default.NotificationsActive,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
stringResource(R.string.enable_notification_access),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Text(
stringResource(R.string.notification_access_permission_details),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
confirmButton = {
Button(
onClick = {
openNotificationListenerSettings(context)
showPermissionDialog = false
},
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.open_settings))
}
},
dismissButton = {
TextButton(
onClick = { showPermissionDialog = false },
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.cancel))
}
},
shape = MaterialTheme.shapes.extraLarge
)
}
}
private fun isNotificationListenerEnabled(context: Context): Boolean {
val cn = ComponentName(context, DolbyNotificationListener::class.java)
val flat = Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners")
return flat?.contains(cn.flattenToString()) == true
}
private fun openNotificationListenerSettings(context: Context) {
val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
context.startActivity(intent)
}

View File

@@ -0,0 +1,287 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlinx.coroutines.launch
import org.lunaris.dolby.R
import org.lunaris.dolby.utils.*
import kotlin.math.absoluteValue
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ProfileCarousel(
currentProfile: Int,
onProfileChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val profiles = stringArrayResource(R.array.dolby_profile_entries)
val profileValues = stringArrayResource(R.array.dolby_profile_values)
val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope()
val profileIcons = mapOf(
0 to Icons.Default.AutoAwesome,
1 to Icons.Default.Movie,
2 to Icons.Default.MusicNote,
3 to Icons.Default.SportsEsports,
4 to Icons.Default.Work,
5 to Icons.Default.Coffee,
6 to Icons.Default.Favorite
)
val profileGradients = listOf(
listOf(Color(0xFF667eea), Color(0xFF764ba2)),
listOf(Color(0xFFf093fb), Color(0xFFf5576c)),
listOf(Color(0xFF4facfe), Color(0xFF00f2fe)),
listOf(Color(0xFFfa709a), Color(0xFFfee140)),
listOf(Color(0xFF30cfd0), Color(0xFF330867)),
listOf(Color(0xFFff9a56), Color(0xFFff6a88)),
listOf(Color(0xFFa18cd1), Color(0xFFfbc2eb))
)
val initialPage = profileValues.indexOfFirst { it.toInt() == currentProfile }.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = initialPage,
pageCount = { profiles.size }
)
var lastPage by remember { mutableIntStateOf(initialPage) }
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != lastPage) {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.DOUBLE_CLICK)
lastPage = pagerState.currentPage
if (pagerState.currentPage != initialPage) {
val selectedValue = profileValues[pagerState.currentPage].toInt()
onProfileChange(selectedValue)
}
}
}
Card(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceBright
)
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.dolby_profile_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 12.dp)
)
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.height(140.dp),
contentPadding = PaddingValues(horizontal = 64.dp),
pageSpacing = 8.dp
) { page ->
val profileValue = profileValues[page].toInt()
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
ProfileCard(
profile = profiles[page],
icon = profileIcons[profileValue] ?: Icons.Default.Tune,
gradient = profileGradients.getOrElse(profileValue) { profileGradients[0] },
isSelected = page == pagerState.currentPage,
pageOffset = pageOffset,
onClick = {
scope.launch {
haptic.performHaptic(HapticFeedbackHelper.HapticIntensity.DOUBLE_CLICK)
pagerState.animateScrollToPage(page)
}
}
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
repeat(profiles.size) { index ->
val isSelected = pagerState.currentPage == index
Box(
modifier = Modifier
.padding(3.dp)
.size(
width = if (isSelected) 24.dp else 6.dp,
height = 6.dp
)
.clip(CircleShape)
.background(
if (isSelected)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.surfaceVariant
)
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun ProfileCard(
profile: String,
icon: ImageVector,
gradient: List<Color>,
isSelected: Boolean,
pageOffset: Float,
onClick: () -> Unit
) {
val scale = lerp(
start = 0.8f,
stop = 1f,
fraction = 1f - pageOffset.absoluteValue.coerceIn(0f, 1f)
)
val alpha = lerp(
start = 0.5f,
stop = 1f,
fraction = 1f - pageOffset.absoluteValue.coerceIn(0f, 1f)
)
Card(
onClick = onClick,
modifier = Modifier
.fillMaxHeight()
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
},
shape = MaterialTheme.shapes.extraLarge,
elevation = CardDefaults.cardElevation(
defaultElevation = if (isSelected) 8.dp else 2.dp
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = gradient
)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val iconScale by animateFloatAsState(
targetValue = if (isSelected) 1f else 0.85f,
animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
label = "icon_scale"
)
Surface(
modifier = Modifier
.size(56.dp)
.scale(iconScale),
shape = CircleShape,
color = Color.White.copy(alpha = 0.3f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(36.dp)
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = profile,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(4.dp))
if (isSelected) {
Surface(
modifier = Modifier.height(2.dp).width(24.dp),
shape = CircleShape,
color = Color.White
) {}
}
}
if (isSelected) {
Surface(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(10.dp)
.size(24.dp),
shape = CircleShape,
color = Color.White
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = gradient[0],
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
}

View File

@@ -0,0 +1,423 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.screens
import androidx.compose.animation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import androidx.navigation.NavController
import org.lunaris.dolby.R
import org.lunaris.dolby.data.AppInfo
import org.lunaris.dolby.domain.models.AppProfileUiState
import org.lunaris.dolby.ui.components.ModernConfirmDialog
import org.lunaris.dolby.ui.viewmodel.AppProfileViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppProfileScreen(
viewModel: AppProfileViewModel,
navController: NavController
) {
val uiState by viewModel.uiState.collectAsState()
var searchQuery by remember { mutableStateOf("") }
var showClearAllDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.app_profiles_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
if (uiState is AppProfileUiState.Success) {
val state = uiState as AppProfileUiState.Success
if (state.appsWithProfiles.isNotEmpty()) {
IconButton(onClick = { showClearAllDialog = true }) {
Icon(
Icons.Default.ClearAll,
contentDescription = "Clear All",
tint = MaterialTheme.colorScheme.error
)
}
}
}
IconButton(onClick = { viewModel.loadApps() }) {
Icon(
Icons.Default.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
},
containerColor = MaterialTheme.colorScheme.surfaceContainer
) { paddingValues ->
when (val state = uiState) {
is AppProfileUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Text(
text = stringResource(R.string.app_profiles_loading),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
is AppProfileUiState.Success -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Search,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(12.dp))
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = {
Text(
stringResource(R.string.app_profiles_search_placeholder),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
modifier = Modifier.weight(1f),
colors = TextFieldDefaults.colors(
focusedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
unfocusedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
focusedIndicatorColor = androidx.compose.ui.graphics.Color.Transparent,
unfocusedIndicatorColor = androidx.compose.ui.graphics.Color.Transparent,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
unfocusedTextColor = MaterialTheme.colorScheme.onSurface
),
singleLine = true
)
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { searchQuery = "" }) {
Icon(
Icons.Default.Clear,
contentDescription = "Clear search",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
val filteredApps = state.apps.filter { app ->
app.appName.contains(searchQuery, ignoreCase = true) ||
app.packageName.contains(searchQuery, ignoreCase = true)
}
if (filteredApps.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.SearchOff,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.app_profiles_no_apps_found),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredApps, key = { it.packageName }) { app ->
AppProfileItem(
app = app,
onProfileSelected = { profile ->
viewModel.setAppProfile(app.packageName, profile)
}
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}
is AppProfileUiState.Error -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.error
)
Text(
text = state.message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
Button(
onClick = { viewModel.loadApps() },
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.app_profiles_retry))
}
}
}
}
}
}
if (showClearAllDialog) {
ModernConfirmDialog(
title = "Clear All App Profiles",
message = "This will remove all per-app profile assignments. Apps will use the default profile.",
icon = Icons.Default.ClearAll,
onConfirm = {
viewModel.clearAllAppProfiles()
showClearAllDialog = false
},
onDismiss = { showClearAllDialog = false }
)
}
}
@Composable
private fun AppProfileItem(
app: AppInfo,
onProfileSelected: (Int) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
val profiles = stringArrayResource(R.array.dolby_profile_entries)
val profileValues = stringArrayResource(R.array.dolby_profile_values)
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = if (app.assignedProfile >= 0) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
} else {
MaterialTheme.colorScheme.surfaceBright
}
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
app.icon?.let { icon ->
Image(
bitmap = icon.toBitmap().asImageBitmap(),
contentDescription = app.appName,
modifier = Modifier
.size(48.dp)
.clip(MaterialTheme.shapes.medium)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = app.appName,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
val currentProfileName = if (app.assignedProfile >= 0) {
val index = profileValues.indexOfFirst { it.toInt() == app.assignedProfile }
if (index >= 0) profiles[index] else "Default"
} else {
"Default"
}
Text(
text = currentProfileName,
style = MaterialTheme.typography.bodySmall,
color = if (app.assignedProfile >= 0) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
Box {
Surface(
onClick = { expanded = true },
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.app_profiles_change),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.ArrowDropDown,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface
)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.app_profiles_default),
color = MaterialTheme.colorScheme.onSurface
)
if (app.assignedProfile == -1) {
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
},
onClick = {
onProfileSelected(-1)
expanded = false
}
)
Divider(color = MaterialTheme.colorScheme.outlineVariant)
profiles.forEachIndexed { index, profileName ->
val profileValue = profileValues[index].toInt()
DropdownMenuItem(
text = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
profileName,
color = MaterialTheme.colorScheme.onSurface
)
if (app.assignedProfile == profileValue) {
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
},
onClick = {
onProfileSelected(profileValue)
expanded = false
}
)
}
}
}
}
}
}

View File

@@ -0,0 +1,357 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.screens
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import org.lunaris.dolby.R
import org.lunaris.dolby.domain.models.DolbyUiState
import org.lunaris.dolby.ui.components.*
import org.lunaris.dolby.ui.viewmodel.DolbyViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ModernAdvancedSettingsScreen(
viewModel: DolbyViewModel,
navController: NavController
) {
val uiState by viewModel.uiState.collectAsState()
val currentRoute by navController.currentBackStackEntryFlow.collectAsState(null)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.dolby_category_adv_settings),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
},
bottomBar = {
BottomNavigationBar(
currentRoute = currentRoute?.destination?.route ?: Screen.Advanced.route,
onNavigate = { route ->
if (currentRoute?.destination?.route != route) {
navController.navigate(route) {
popUpTo(Screen.Settings.route) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
}
)
},
containerColor = MaterialTheme.colorScheme.surfaceContainer
) { paddingValues ->
when (val state = uiState) {
is DolbyUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
is DolbyUiState.Success -> {
ModernAdvancedSettingsContent(
state = state,
viewModel = viewModel,
navController = navController,
modifier = Modifier.padding(paddingValues)
)
}
is DolbyUiState.Error -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.error
)
Text(
text = state.message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
}
@Composable
private fun ModernAdvancedSettingsContent(
state: DolbyUiState.Success,
viewModel: DolbyViewModel,
navController: NavController,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (state.settings.enabled) {
item {
ModernSettingsCard(
title = stringResource(R.string.dolby_category_settings),
icon = Icons.Default.Tune
) {
Column {
ModernSettingSwitch(
title = stringResource(R.string.dolby_bass_enhancer),
subtitle = stringResource(R.string.dolby_bass_enhancer_summary),
checked = state.profileSettings.bassLevel > 0,
onCheckedChange = { enabled ->
if (enabled && state.profileSettings.bassLevel == 0) {
viewModel.setBassLevel(50)
} else if (!enabled) {
viewModel.setBassLevel(0)
}
},
icon = Icons.Default.MusicNote
)
AnimatedVisibility(visible = state.profileSettings.bassLevel > 0) {
Column {
Spacer(modifier = Modifier.height(8.dp))
ModernSettingSelector(
title = stringResource(R.string.dolby_bass_curve),
currentValue = state.profileSettings.bassCurve,
entries = R.array.dolby_bass_curve_entries,
values = R.array.dolby_bass_curve_values,
onValueChange = { viewModel.setBassCurve(it) },
icon = Icons.Default.Equalizer
)
Spacer(modifier = Modifier.height(8.dp))
ModernSettingSlider(
title = stringResource(R.string.dolby_bass_level),
value = state.profileSettings.bassLevel,
onValueChange = { viewModel.setBassLevel(it.toInt()) },
valueRange = 0f..100f,
steps = 19,
valueLabel = { "$it%" }
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Column {
ModernSettingSwitch(
title = stringResource(R.string.dolby_treble_enhancer),
subtitle = stringResource(R.string.dolby_treble_enhancer_summary),
checked = state.profileSettings.trebleLevel > 0,
onCheckedChange = { enabled ->
if (enabled && state.profileSettings.trebleLevel == 0) {
viewModel.setTrebleLevel(30)
} else if (!enabled) {
viewModel.setTrebleLevel(0)
}
},
icon = Icons.Default.GraphicEq
)
AnimatedVisibility(visible = state.profileSettings.trebleLevel > 0) {
Column {
Spacer(modifier = Modifier.height(8.dp))
ModernSettingSlider(
title = stringResource(R.string.dolby_treble_level),
value = state.profileSettings.trebleLevel,
onValueChange = { viewModel.setTrebleLevel(it.toInt()) },
valueRange = 0f..100f,
steps = 19,
valueLabel = { "$it%" }
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
ModernSettingSwitch(
title = stringResource(R.string.dolby_volume_leveler),
subtitle = stringResource(R.string.dolby_volume_leveler_summary),
checked = state.settings.volumeLevelerEnabled,
onCheckedChange = { viewModel.setVolumeLeveler(it) },
icon = Icons.Default.BarChart
)
}
}
if (state.settings.currentProfile != 0) {
item {
ModernSettingsCard(
title = "Surround Virtualizer",
icon = Icons.Default.Headphones
) {
if (state.isOnSpeaker) {
ModernSettingSwitch(
title = stringResource(R.string.dolby_spk_virtualizer),
subtitle = stringResource(R.string.dolby_spk_virtualizer_summary),
checked = state.profileSettings.speakerVirtualizerEnabled,
onCheckedChange = { viewModel.setSpeakerVirtualizer(it) },
icon = Icons.Default.Speaker
)
} else {
ModernSettingSwitch(
title = stringResource(R.string.dolby_hp_virtualizer),
subtitle = stringResource(R.string.dolby_hp_virtualizer_summary),
checked = state.profileSettings.headphoneVirtualizerEnabled,
onCheckedChange = { viewModel.setHeadphoneVirtualizer(it) },
icon = Icons.Default.Headphones
)
AnimatedVisibility(visible = state.profileSettings.headphoneVirtualizerEnabled) {
Column {
Spacer(modifier = Modifier.height(16.dp))
ModernSettingSlider(
title = stringResource(R.string.dolby_hp_virtualizer_dolby_strength),
value = state.profileSettings.stereoWideningAmount,
onValueChange = { viewModel.setStereoWidening(it.toInt()) },
valueRange = 4f..64f,
steps = 59
)
}
}
}
}
}
item {
ModernSettingsCard(
title = "Dialogue Enhancement",
icon = Icons.Default.RecordVoiceOver
) {
ModernSettingSwitch(
title = stringResource(R.string.dolby_dialogue_enhancer),
subtitle = stringResource(R.string.dolby_dialogue_enhancer_summary),
checked = state.profileSettings.dialogueEnhancerEnabled,
onCheckedChange = { viewModel.setDialogueEnhancer(it) },
icon = Icons.Default.RecordVoiceOver
)
AnimatedVisibility(visible = state.profileSettings.dialogueEnhancerEnabled) {
Column {
Spacer(modifier = Modifier.height(16.dp))
ModernSettingSlider(
title = stringResource(R.string.dolby_dialogue_enhancer_dolby_strength),
value = state.profileSettings.dialogueEnhancerAmount,
onValueChange = { viewModel.setDialogueEnhancerAmount(it.toInt()) },
valueRange = 1f..12f,
steps = 10
)
}
}
}
}
}
} else if (state.settings.currentProfile == 0 && state.settings.enabled) {
item {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.dolby_adv_settings_footer),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
} else if (!state.settings.enabled) {
item {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.dolby_adv_settings_footer),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
}
item {
AppProfileSettingsCard(
onManageClick = { navController.navigate("app_profiles") }
)
}
item {
Spacer(modifier = Modifier.height(80.dp))
}
}
}
@Composable
private fun BottomNavigationBar(
currentRoute: String,
onNavigate: (String) -> Unit
) {
EnhancedBottomNavigationBar(
currentRoute = currentRoute,
onNavigate = onNavigate
)
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.screens
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import org.lunaris.dolby.R
import org.lunaris.dolby.domain.models.DolbyUiState
import org.lunaris.dolby.ui.components.*
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ModernDolbySettingsScreen(
viewModel: org.lunaris.dolby.ui.viewmodel.DolbyViewModel,
navController: NavController
) {
val uiState by viewModel.uiState.collectAsState()
var showResetDialog by remember { mutableStateOf(false) }
var showCreditsDialog by remember { mutableStateOf(false) }
val currentRoute by navController.currentBackStackEntryFlow.collectAsState(null)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.dolby_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
actions = {
IconButton(onClick = { showCreditsDialog = true }) {
Icon(
Icons.Default.Info,
contentDescription = "Credits",
tint = MaterialTheme.colorScheme.onSurface
)
}
IconButton(onClick = { showResetDialog = true }) {
Icon(
Icons.Default.RestartAlt,
contentDescription = "Reset",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
},
bottomBar = {
if (uiState is DolbyUiState.Success) {
BottomNavigationBar(
currentRoute = currentRoute?.destination?.route ?: Screen.Settings.route,
onNavigate = { route ->
if (currentRoute?.destination?.route != route) {
navController.navigate(route) {
popUpTo(Screen.Settings.route) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
}
)
}
},
containerColor = MaterialTheme.colorScheme.surfaceContainer
) { paddingValues ->
when (val state = uiState) {
is DolbyUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Text(
text = stringResource(R.string.loading),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
is DolbyUiState.Success -> {
ModernDolbySettingsContent(
state = state,
viewModel = viewModel,
modifier = Modifier.padding(paddingValues)
)
}
is DolbyUiState.Error -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.error
)
Text(
text = state.message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
if (showResetDialog) {
ModernConfirmDialog(
title = stringResource(R.string.dolby_reset_all),
message = stringResource(R.string.dolby_reset_all_message),
icon = Icons.Default.RestartAlt,
onConfirm = {
viewModel.resetAllProfiles()
showResetDialog = false
},
onDismiss = { showResetDialog = false }
)
}
if (showCreditsDialog) {
CreditsDialog(
onDismiss = { showCreditsDialog = false }
)
}
}
@Composable
private fun ModernDolbySettingsContent(
state: DolbyUiState.Success,
viewModel: org.lunaris.dolby.ui.viewmodel.DolbyViewModel,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
DolbyMainCard(
enabled = state.settings.enabled,
onEnabledChange = { viewModel.setDolbyEnabled(it) }
)
}
item {
NotificationListenerPermissionCard()
}
item {
AnimatedVisibility(
visible = state.settings.enabled,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ModernProfileSelector(
currentProfile = state.settings.currentProfile,
onProfileChange = { viewModel.setProfile(it) }
)
}
}
item {
AnimatedVisibility(
visible = state.settings.enabled && state.settings.currentProfile != 0,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ModernSettingsCard(
title = "Intelligent Equalizer",
icon = Icons.Default.GraphicEq
) {
ModernIeqSelector(
currentPreset = state.profileSettings.ieqPreset,
onPresetChange = { viewModel.setIeqPreset(it) }
)
}
}
}
item {
Spacer(modifier = Modifier.height(80.dp))
}
}
}
@Composable
private fun BottomNavigationBar(
currentRoute: String,
onNavigate: (String) -> Unit
) {
EnhancedBottomNavigationBar(
currentRoute = currentRoute,
onNavigate = onNavigate
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.lunaris.dolby.ui.viewmodel.AppProfileViewModel
import org.lunaris.dolby.ui.viewmodel.DolbyViewModel
import org.lunaris.dolby.ui.viewmodel.EqualizerViewModel
sealed class Screen(val route: String) {
object Settings : Screen("settings")
object Equalizer : Screen("equalizer")
object Advanced : Screen("advanced")
object AppProfiles : Screen("app_profiles")
object ImportExport : Screen("import_export")
}
@Composable
fun DolbyNavHost(
dolbyViewModel: DolbyViewModel,
equalizerViewModel: EqualizerViewModel
) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Settings.route
) {
composable(Screen.Settings.route) {
ModernDolbySettingsScreen(
viewModel = dolbyViewModel,
navController = navController
)
}
composable(Screen.Equalizer.route) {
LaunchedEffect(Unit) {
equalizerViewModel.loadEqualizer()
}
ModernEqualizerScreen(
viewModel = equalizerViewModel,
navController = navController
)
}
composable(Screen.Advanced.route) {
ModernAdvancedSettingsScreen(
viewModel = dolbyViewModel,
navController = navController
)
}
composable(Screen.AppProfiles.route) {
val context = LocalContext.current
val appProfileViewModel: AppProfileViewModel = viewModel(
factory = androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.getInstance(
context.applicationContext as android.app.Application
)
)
AppProfileScreen(
viewModel = appProfileViewModel,
navController = navController
)
}
composable(Screen.ImportExport.route) {
PresetImportExportScreen(
viewModel = equalizerViewModel,
navController = navController
)
}
}
}

View File

@@ -0,0 +1,550 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.screens
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import org.lunaris.dolby.R
import org.lunaris.dolby.data.PresetExportManager
import org.lunaris.dolby.domain.models.EqualizerPreset
import org.lunaris.dolby.domain.models.EqualizerUiState
import org.lunaris.dolby.ui.components.ModernConfirmDialog
import org.lunaris.dolby.ui.viewmodel.EqualizerViewModel
import org.lunaris.dolby.utils.ToastHelper
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun PresetImportExportScreen(
viewModel: EqualizerViewModel,
navController: NavController
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val exportManager = remember { PresetExportManager(context) }
val uiState by viewModel.uiState.collectAsState()
var selectedPreset by remember { mutableStateOf<EqualizerPreset?>(null) }
var showExportOptions by remember { mutableStateOf(false) }
var showBatchExport by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
var presetToDelete by remember { mutableStateOf<EqualizerPreset?>(null) }
var isLoading by remember { mutableStateOf(false) }
val exportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
selectedPreset?.let { preset ->
scope.launch {
isLoading = true
exportManager.exportPresetToFile(preset, uri).fold(
onSuccess = {
ToastHelper.showToast(context, "Preset exported successfully!")
},
onFailure = { e ->
ToastHelper.showToast(context, "Export failed: ${e.message}")
}
)
isLoading = false
showExportOptions = false
}
}
}
}
val importLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
scope.launch {
isLoading = true
exportManager.importPresetFromFile(uri).fold(
onSuccess = { preset ->
val error = viewModel.saveImportedPreset(preset)
if (error != null) {
ToastHelper.showToast(context, error)
} else {
ToastHelper.showToast(
context,
"Preset '${preset.name}' imported! (${preset.bandMode.displayName})"
)
viewModel.loadEqualizer()
}
},
onFailure = { e ->
ToastHelper.showToast(context, "Import failed: ${e.message}")
}
)
isLoading = false
}
}
}
val batchExportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
scope.launch {
isLoading = true
val state = uiState as? EqualizerUiState.Success
val presets = state?.presets?.filter { it.isUserDefined } ?: emptyList()
exportManager.exportMultiplePresets(presets, uri).fold(
onSuccess = {
ToastHelper.showToast(context, "${presets.size} presets exported!")
},
onFailure = { e ->
ToastHelper.showToast(context, "Batch export failed: ${e.message}")
}
)
isLoading = false
showBatchExport = false
}
}
}
val batchImportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
scope.launch {
isLoading = true
exportManager.importMultiplePresets(uri).fold(
onSuccess = { presets ->
var successCount = 0
presets.forEach { preset ->
if (viewModel.saveImportedPreset(preset) == null) {
successCount++
}
}
ToastHelper.showToast(
context,
"Imported $successCount of ${presets.size} presets"
)
viewModel.loadEqualizer()
},
onFailure = { e ->
ToastHelper.showToast(context, "Batch import failed: ${e.message}")
}
)
isLoading = false
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.import_export_presets),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
IconButton(onClick = { showBatchExport = true }) {
Icon(
Icons.Default.FileDownload,
contentDescription = "Batch export",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
},
containerColor = MaterialTheme.colorScheme.surfaceContainer
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
when (val state = uiState) {
is EqualizerUiState.Success -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceBright
)
) {
Column(modifier = Modifier.padding(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 16.dp)
) {
Surface(
modifier = Modifier.size(40.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
Icons.Default.FileDownload,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
stringResource(R.string.import_presets),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
Text(
stringResource(R.string.import_presets_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 16.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { importLauncher.launch("*/*") },
modifier = Modifier.weight(1f),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Icon(Icons.Default.FolderOpen, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.import_single_file))
}
Button(
onClick = { batchImportLauncher.launch("*/*") },
modifier = Modifier.weight(1f),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Icon(Icons.Default.FolderCopy, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.import_batch))
}
}
Spacer(Modifier.height(8.dp))
OutlinedButton(
onClick = {
scope.launch {
isLoading = true
exportManager.importPresetFromClipboard().fold(
onSuccess = { preset ->
val error = viewModel.saveImportedPreset(preset)
if (error != null) {
ToastHelper.showToast(context, error)
} else {
ToastHelper.showToast(
context,
"Preset imported from clipboard! (${preset.bandMode.displayName})"
)
viewModel.loadEqualizer()
}
},
onFailure = { e ->
ToastHelper.showToast(
context,
"Clipboard import failed: ${e.message}"
)
}
)
isLoading = false
}
},
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
) {
Icon(Icons.Default.ContentPaste, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.import_from_clipboard))
}
}
}
}
item {
Text(
stringResource(R.string.your_custom_presets),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(vertical = 8.dp)
)
}
items(state.presets.filter { it.isUserDefined }) { preset ->
PresetExportCard(
preset = preset,
onExportFile = {
selectedPreset = preset
exportLauncher.launch("${preset.name.replace(" ", "_")}_${preset.bandMode.value}band.ldp")
},
onCopyClipboard = {
scope.launch {
isLoading = true
exportManager.copyPresetToClipboard(preset).fold(
onSuccess = {
ToastHelper.showToast(
context,
"Preset copied to clipboard!"
)
},
onFailure = { e ->
ToastHelper.showToast(
context,
"Copy failed: ${e.message}"
)
}
)
isLoading = false
}
},
onShare = {
scope.launch {
isLoading = true
exportManager.createShareIntent(preset).fold(
onSuccess = { intent ->
context.startActivity(intent)
},
onFailure = { e ->
ToastHelper.showToast(
context,
"Share failed: ${e.message}"
)
}
)
isLoading = false
}
},
onDelete = {
presetToDelete = preset
showDeleteDialog = true
}
)
}
item {
Spacer(Modifier.height(80.dp))
}
}
}
else -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
}
if (isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 8.dp
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(16.dp))
Text(
stringResource(R.string.processing),
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
}
}
if (showBatchExport) {
val state = uiState as? EqualizerUiState.Success
val presetCount = state?.presets?.count { it.isUserDefined } ?: 0
AlertDialog(
onDismissRequest = { showBatchExport = false },
icon = {
Icon(
Icons.Default.FileDownload,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
stringResource(R.string.batch_export),
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Text(
stringResource(R.string.batch_export_description, presetCount),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
confirmButton = {
Button(
onClick = {
batchExportLauncher.launch("dolby_presets_backup.ldp")
},
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.export_all))
}
},
dismissButton = {
TextButton(
onClick = { showBatchExport = false },
shape = MaterialTheme.shapes.medium
) {
Text(stringResource(R.string.cancel))
}
},
shape = MaterialTheme.shapes.extraLarge
)
}
if (showDeleteDialog && presetToDelete != null) {
ModernConfirmDialog(
title = "Delete Preset",
message = "Are you sure you want to delete '${presetToDelete!!.name}'? This will remove it from your preset list and cannot be undone.",
icon = Icons.Default.Delete,
onConfirm = {
scope.launch {
viewModel.deletePreset(presetToDelete!!)
ToastHelper.showToast(context, "Preset '${presetToDelete!!.name}' deleted")
viewModel.loadEqualizer()
showDeleteDialog = false
presetToDelete = null
}
},
onDismiss = {
showDeleteDialog = false
presetToDelete = null
}
)
}
}
@Composable
private fun PresetExportCard(
preset: EqualizerPreset,
onExportFile: () -> Unit,
onCopyClipboard: () -> Unit,
onShare: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceBright
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
preset.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
"${preset.bandMode.displayName}${preset.bandGains.size} bands",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
IconButton(onClick = onExportFile) {
Icon(
Icons.Default.FileDownload,
contentDescription = "Export to file",
tint = MaterialTheme.colorScheme.primary
)
}
IconButton(onClick = onCopyClipboard) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy to clipboard",
tint = MaterialTheme.colorScheme.secondary
)
}
IconButton(onClick = onShare) {
Icon(
Icons.Default.Share,
contentDescription = "Share",
tint = MaterialTheme.colorScheme.tertiary
)
}
IconButton(onClick = onDelete) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
private val ExpressiveShapes = Shapes(
extraSmall = RoundedCornerShape(8.dp),
small = RoundedCornerShape(12.dp),
medium = RoundedCornerShape(16.dp),
large = RoundedCornerShape(24.dp),
extraLarge = RoundedCornerShape(32.dp)
)
private val DolbyTypography = Typography(
displayLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
headlineLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DolbyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.surface.toArgb()
window.navigationBarColor = colorScheme.surface.toArgb()
WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = !darkTheme
isAppearanceLightNavigationBars = !darkTheme
}
}
}
MaterialExpressiveTheme(
colorScheme = colorScheme,
motionScheme = MotionScheme.expressive(),
shapes = ExpressiveShapes,
typography = DolbyTypography,
content = content
)
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.R
import org.lunaris.dolby.data.AppProfileManager
import org.lunaris.dolby.domain.models.AppProfileUiState
import org.lunaris.dolby.utils.ToastHelper
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.cancelChildren
class AppProfileViewModel(application: Application) : AndroidViewModel(application) {
private val appProfileManager = AppProfileManager(application)
private val context = application
private val _uiState = MutableStateFlow<AppProfileUiState>(AppProfileUiState.Loading)
val uiState: StateFlow<AppProfileUiState> = _uiState.asStateFlow()
private var isCleared = false
init {
DolbyConstants.dlog(TAG, "ViewModel initialized")
loadApps()
}
fun loadApps() {
if (isCleared) {
DolbyConstants.dlog(TAG, "ViewModel cleared, skipping loadApps")
return
}
viewModelScope.launch {
try {
_uiState.value = AppProfileUiState.Loading
val apps = appProfileManager.getInstalledApps()
val appsWithProfiles = appProfileManager.getAppsWithProfiles()
if (!isCleared) {
_uiState.value = AppProfileUiState.Success(
apps = apps,
appsWithProfiles = appsWithProfiles
)
}
} catch (e: Exception) {
if (!isCleared) {
DolbyConstants.dlog(TAG, "Error loading apps: ${e.message}")
_uiState.value = AppProfileUiState.Error(e.message ?: "Unknown error")
}
}
}
}
fun setAppProfile(packageName: String, profile: Int) {
viewModelScope.launch {
try {
if (profile == -1) {
appProfileManager.removeAppProfile(packageName)
ToastHelper.showToast(context, "Profile reset to default")
} else {
appProfileManager.setAppProfile(packageName, profile)
val profileName = getProfileName(profile)
ToastHelper.showToast(context, "Profile set to: $profileName")
}
loadApps()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting app profile: ${e.message}")
}
}
}
fun removeAppProfile(packageName: String) {
viewModelScope.launch {
try {
appProfileManager.removeAppProfile(packageName)
ToastHelper.showToast(context, "Profile removed")
loadApps()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error removing app profile: ${e.message}")
}
}
}
fun clearAllAppProfiles() {
viewModelScope.launch {
try {
appProfileManager.clearAllAppProfiles()
ToastHelper.showToast(context, "All app profiles cleared")
loadApps()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error clearing app profiles: ${e.message}")
}
}
}
private fun getProfileName(profile: Int): String {
return try {
val profiles = context.resources.getStringArray(R.array.dolby_profile_entries)
val profileValues = context.resources.getStringArray(R.array.dolby_profile_values)
val index = profileValues.indexOfFirst { it.toInt() == profile }
if (index >= 0) profiles[index] else "Unknown"
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error getting profile name: ${e.message}")
"Unknown"
}
}
override fun onCleared() {
DolbyConstants.dlog(TAG, "ViewModel onCleared")
isCleared = true
viewModelScope.coroutineContext.cancelChildren()
super.onCleared()
}
companion object {
private const val TAG = "AppProfileViewModel"
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.data.DolbyRepository
import org.lunaris.dolby.domain.models.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.cancelChildren
class DolbyViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DolbyRepository(application)
private val _uiState = MutableStateFlow<DolbyUiState>(DolbyUiState.Loading)
val uiState: StateFlow<DolbyUiState> = _uiState.asStateFlow()
val currentProfile: StateFlow<Int> = repository.currentProfile
private var speakerStateJob: Job? = null
private var profileChangeJob: Job? = null
private var isCleared = false
init {
DolbyConstants.dlog(TAG, "ViewModel initialized")
loadSettings()
observeSpeakerState()
observeProfileChanges()
}
private fun observeSpeakerState() {
speakerStateJob?.cancel()
speakerStateJob = viewModelScope.launch {
repository.isOnSpeaker.collect {
if (!isCleared) {
DolbyConstants.dlog(TAG, "Speaker state changed: $it")
loadSettings()
}
}
}
}
private fun observeProfileChanges() {
profileChangeJob?.cancel()
profileChangeJob = viewModelScope.launch {
repository.currentProfile.collect {
if (!isCleared) {
DolbyConstants.dlog(TAG, "Profile changed to: $it")
loadSettings()
}
}
}
}
fun loadSettings() {
if (isCleared) {
DolbyConstants.dlog(TAG, "ViewModel cleared, skipping loadSettings")
return
}
viewModelScope.launch {
try {
val enabled = repository.getDolbyEnabled()
val profile = repository.getCurrentProfile()
val bandMode = repository.getBandMode()
val settings = DolbySettings(
enabled = enabled,
currentProfile = profile,
bassEnhancerEnabled = repository.getBassEnhancerEnabled(profile),
volumeLevelerEnabled = repository.getVolumeLevelerEnabled(profile),
bandMode = bandMode
)
val profileSettings = ProfileSettings(
profile = profile,
ieqPreset = repository.getIeqPreset(profile),
headphoneVirtualizerEnabled = repository.getHeadphoneVirtualizerEnabled(profile),
speakerVirtualizerEnabled = repository.getSpeakerVirtualizerEnabled(profile),
stereoWideningAmount = repository.getStereoWideningAmount(profile),
dialogueEnhancerEnabled = repository.getDialogueEnhancerEnabled(profile),
dialogueEnhancerAmount = repository.getDialogueEnhancerAmount(profile),
bassLevel = repository.getBassLevel(profile),
trebleLevel = repository.getTrebleLevel(profile),
bassCurve = repository.getBassCurve(profile)
)
if (!isCleared) {
_uiState.value = DolbyUiState.Success(
settings = settings,
profileSettings = profileSettings,
currentPresetName = repository.getPresetName(profile),
isOnSpeaker = repository.isOnSpeaker.value
)
}
} catch (e: Exception) {
if (!isCleared) {
DolbyConstants.dlog(TAG, "Error loading settings: ${e.message}")
_uiState.value = DolbyUiState.Error(e.message ?: "Unknown error")
}
}
}
}
fun setDolbyEnabled(enabled: Boolean) {
viewModelScope.launch {
try {
repository.setDolbyEnabled(enabled)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting Dolby enabled: ${e.message}")
}
}
}
fun setProfile(profile: Int) {
viewModelScope.launch {
try {
repository.setCurrentProfile(profile)
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting profile: ${e.message}")
}
}
}
fun setBassEnhancer(enabled: Boolean) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setBassEnhancerEnabled(profile, enabled)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting bass enhancer: ${e.message}")
}
}
}
fun setBassLevel(level: Int) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setBassLevel(profile, level)
loadSettings()
} catch (e: IllegalArgumentException) {
DolbyConstants.dlog(TAG, "Invalid bass level: ${e.message}")
_uiState.value = DolbyUiState.Error("Invalid bass level: ${e.message}")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting bass level: ${e.message}")
_uiState.value = DolbyUiState.Error("Failed to set bass level")
}
}
}
fun setBassCurve(curve: Int) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setBassCurve(profile, curve)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting bass curve: ${e.message}")
}
}
}
fun setTrebleLevel(level: Int) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setTrebleLevel(profile, level)
loadSettings()
} catch (e: IllegalArgumentException) {
DolbyConstants.dlog(TAG, "Invalid treble level: ${e.message}")
_uiState.value = DolbyUiState.Error("Invalid treble level: ${e.message}")
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting treble level: ${e.message}")
_uiState.value = DolbyUiState.Error("Failed to set treble level")
}
}
}
fun setVolumeLeveler(enabled: Boolean) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setVolumeLevelerEnabled(profile, enabled)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting volume leveler: ${e.message}")
}
}
}
fun setIeqPreset(preset: Int) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setIeqPreset(profile, preset)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting IEQ preset: ${e.message}")
}
}
}
fun setHeadphoneVirtualizer(enabled: Boolean) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setHeadphoneVirtualizerEnabled(profile, enabled)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting headphone virtualizer: ${e.message}")
}
}
}
fun setSpeakerVirtualizer(enabled: Boolean) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setSpeakerVirtualizerEnabled(profile, enabled)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting speaker virtualizer: ${e.message}")
}
}
}
fun setStereoWidening(amount: Int) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setStereoWideningAmount(profile, amount)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting stereo widening: ${e.message}")
}
}
}
fun setDialogueEnhancer(enabled: Boolean) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setDialogueEnhancerEnabled(profile, enabled)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting dialogue enhancer: ${e.message}")
}
}
}
fun setDialogueEnhancerAmount(amount: Int) {
viewModelScope.launch {
try {
val profile = repository.getCurrentProfile()
repository.setDialogueEnhancerAmount(profile, amount)
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting dialogue enhancer amount: ${e.message}")
}
}
}
fun resetAllProfiles() {
viewModelScope.launch {
try {
repository.resetAllProfiles()
loadSettings()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error resetting profiles: ${e.message}")
}
}
}
fun updateSpeakerState() {
if (!isCleared) {
repository.updateSpeakerState()
}
}
override fun onCleared() {
DolbyConstants.dlog(TAG, "ViewModel onCleared")
isCleared = true
viewModelScope.coroutineContext.cancelChildren()
speakerStateJob?.cancel()
speakerStateJob = null
profileChangeJob?.cancel()
profileChangeJob = null
repository.close()
super.onCleared()
}
companion object {
private const val TAG = "DolbyViewModel"
}
}

View File

@@ -0,0 +1,345 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import org.lunaris.dolby.DolbyConstants
import org.lunaris.dolby.R
import org.lunaris.dolby.data.DolbyRepository
import org.lunaris.dolby.domain.models.*
import org.lunaris.dolby.utils.ToastHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.cancelChildren
class EqualizerViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DolbyRepository(application)
private val context = application
private val _uiState = MutableStateFlow<EqualizerUiState>(EqualizerUiState.Loading)
val uiState: StateFlow<EqualizerUiState> = _uiState.asStateFlow()
private var currentProfile = 0
private var currentBandMode = BandMode.TEN_BAND
private var profileChangeJob: Job? = null
private var isCleared = false
init {
DolbyConstants.dlog(TAG, "ViewModel initialized")
loadEqualizer()
observeProfileChanges()
}
private fun observeProfileChanges() {
profileChangeJob?.cancel()
profileChangeJob = viewModelScope.launch {
repository.currentProfile.collect {
if (!isCleared) {
DolbyConstants.dlog(TAG, "Profile changed, reloading equalizer")
loadEqualizer()
}
}
}
}
fun loadEqualizer() {
if (isCleared) {
DolbyConstants.dlog(TAG, "ViewModel cleared, skipping loadEqualizer")
return
}
viewModelScope.launch {
try {
currentProfile = repository.getCurrentProfile()
currentBandMode = repository.getBandMode()
val bandGains = repository.getEqualizerGains(currentProfile, currentBandMode)
val builtInPresets = getBuiltInPresets(currentBandMode)
val userPresets = repository.getUserPresets()
val allPresets = userPresets + builtInPresets
val currentPresetName = repository.getPresetName(currentProfile)
val currentPreset = allPresets.find { it.name == currentPresetName }
?: EqualizerPreset(
name = context.getString(R.string.dolby_preset_custom),
bandGains = bandGains,
isCustom = true,
bandMode = currentBandMode
)
if (!isCleared) {
_uiState.value = EqualizerUiState.Success(
presets = allPresets,
currentPreset = currentPreset,
bandGains = bandGains,
bandMode = currentBandMode
)
}
} catch (e: Exception) {
if (!isCleared) {
DolbyConstants.dlog(TAG, "Error loading equalizer: ${e.message}")
_uiState.value = EqualizerUiState.Error(e.message ?: "Unknown error")
}
}
}
}
private fun getBuiltInPresets(bandMode: BandMode): List<EqualizerPreset> {
val names = context.resources.getStringArray(R.array.dolby_preset_entries)
val values = context.resources.getStringArray(R.array.dolby_preset_values)
return names.mapIndexed { index, name ->
val gains = values[index].split(",").map { it.toInt() }
val frequencies = when (bandMode) {
BandMode.TEN_BAND -> DolbyRepository.BAND_FREQUENCIES_10
BandMode.FIFTEEN_BAND -> DolbyRepository.BAND_FREQUENCIES_15
BandMode.TWENTY_BAND -> DolbyRepository.BAND_FREQUENCIES_20
}
val tenBandGains = gains.filterIndexed { i, _ -> i % 2 == 0 }
val targetGains = when (bandMode) {
BandMode.TEN_BAND -> tenBandGains
BandMode.FIFTEEN_BAND -> {
val result = mutableListOf<Int>()
result.add(tenBandGains[0])
result.add(tenBandGains[0])
result.add(tenBandGains[0])
result.add(tenBandGains[1])
result.add(tenBandGains[2])
result.add(tenBandGains[3])
result.add(tenBandGains[4])
result.add((tenBandGains[4] + tenBandGains[5]) / 2)
result.add(tenBandGains[5])
result.add((tenBandGains[5] + tenBandGains[6]) / 2)
result.add(tenBandGains[6])
result.add((tenBandGains[6] + tenBandGains[7]) / 2)
result.add(tenBandGains[7])
result.add((tenBandGains[7] + tenBandGains[8]) / 2)
result.add(tenBandGains[9])
result
}
BandMode.TWENTY_BAND -> gains
}
EqualizerPreset(
name = name,
bandGains = frequencies.mapIndexed { i, freq ->
BandGain(frequency = freq, gain = targetGains.getOrElse(i) { 0 })
},
bandMode = bandMode
)
}
}
fun setBandMode(mode: BandMode) {
viewModelScope.launch {
try {
repository.setBandMode(mode)
currentBandMode = mode
loadEqualizer()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting band mode: ${e.message}")
}
}
}
fun setPreset(preset: EqualizerPreset) {
viewModelScope.launch {
try {
val targetGains = if (preset.bandMode != currentBandMode) {
convertPresetToBandMode(preset, currentBandMode)
} else {
preset.bandGains
}
repository.setEqualizerGains(currentProfile, targetGains, currentBandMode)
loadEqualizer()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting preset: ${e.message}")
}
}
}
private fun convertPresetToBandMode(preset: EqualizerPreset, targetMode: BandMode): List<BandGain> {
val sourceFreqs = when (preset.bandMode) {
BandMode.TEN_BAND -> DolbyRepository.BAND_FREQUENCIES_10
BandMode.FIFTEEN_BAND -> DolbyRepository.BAND_FREQUENCIES_15
BandMode.TWENTY_BAND -> DolbyRepository.BAND_FREQUENCIES_20
}
val targetFreqs = when (targetMode) {
BandMode.TEN_BAND -> DolbyRepository.BAND_FREQUENCIES_10
BandMode.FIFTEEN_BAND -> DolbyRepository.BAND_FREQUENCIES_15
BandMode.TWENTY_BAND -> DolbyRepository.BAND_FREQUENCIES_20
}
if (preset.bandGains.size != sourceFreqs.size) {
DolbyConstants.dlog(TAG,
"Preset band count mismatch: expected ${sourceFreqs.size}, got ${preset.bandGains.size}")
return targetFreqs.map { BandGain(frequency = it, gain = 0) }
}
return targetFreqs.map { targetFreq ->
val closestIdx = sourceFreqs.indexOfFirst { it >= targetFreq }
val gain = when {
closestIdx == -1 -> {
preset.bandGains.lastOrNull()?.gain ?: 0
}
closestIdx == 0 -> {
preset.bandGains.firstOrNull()?.gain ?: 0
}
else -> {
val prevFreq = sourceFreqs[closestIdx - 1]
val nextFreq = sourceFreqs[closestIdx]
val prevGain = preset.bandGains.getOrNull(closestIdx - 1)?.gain ?: 0
val nextGain = preset.bandGains.getOrNull(closestIdx)?.gain ?: 0
val ratio = (targetFreq - prevFreq).toFloat() / (nextFreq - prevFreq)
(prevGain + ratio * (nextGain - prevGain)).toInt()
}
}
BandGain(frequency = targetFreq, gain = gain)
}
}
fun canEditCurrentPreset(): Boolean {
val state = _uiState.value
if (state is EqualizerUiState.Success) {
return state.currentPreset.bandMode == currentBandMode
}
return false
}
fun getCurrentPresetBandMode(): BandMode? {
val state = _uiState.value
if (state is EqualizerUiState.Success) {
return state.currentPreset.bandMode
}
return null
}
fun setBandGain(index: Int, gain: Int) {
viewModelScope.launch {
try {
val state = _uiState.value
if (state is EqualizerUiState.Success) {
val isFlatPreset = state.currentPreset.name == context.getString(R.string.dolby_preset_default)
if (!isFlatPreset && state.currentPreset.bandMode != currentBandMode) {
ToastHelper.showToast(
context,
"Cannot edit ${state.currentPreset.bandMode.displayName} preset in ${currentBandMode.displayName} mode. " +
"Switch to ${state.currentPreset.bandMode.displayName} or select a different preset."
)
return@launch
}
val newBandGains = state.bandGains.toMutableList()
newBandGains[index] = newBandGains[index].copy(gain = gain)
repository.setEqualizerGains(currentProfile, newBandGains, currentBandMode)
loadEqualizer()
}
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error setting band gain: ${e.message}")
}
}
}
fun savePreset(name: String): String? {
val state = _uiState.value
if (state !is EqualizerUiState.Success) return "Invalid state"
if (state.presets.any { it.name.equals(name.trim(), ignoreCase = true) }) {
return context.getString(R.string.dolby_geq_preset_name_exists)
}
if (name.length > 50) {
return context.getString(R.string.dolby_geq_preset_name_too_long)
}
viewModelScope.launch {
try {
repository.addUserPreset(name.trim(), state.bandGains, currentBandMode)
loadEqualizer()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error saving preset: ${e.message}")
}
}
return null
}
fun deletePreset(preset: EqualizerPreset) {
if (!preset.isUserDefined) return
viewModelScope.launch {
try {
repository.deleteUserPreset(preset.name)
loadEqualizer()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error deleting preset: ${e.message}")
}
}
}
fun saveImportedPreset(preset: EqualizerPreset): String? {
val state = _uiState.value
if (state !is EqualizerUiState.Success) return "Invalid state"
if (state.presets.any { it.name.equals(preset.name.trim(), ignoreCase = true) }) {
return context.getString(R.string.dolby_geq_preset_name_exists)
}
if (preset.name.length > 50) {
return context.getString(R.string.dolby_geq_preset_name_too_long)
}
viewModelScope.launch {
try {
repository.addUserPreset(
preset.name.trim(),
preset.bandGains,
preset.bandMode
)
loadEqualizer()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error saving imported preset: ${e.message}")
}
}
return null
}
fun resetGains() {
viewModelScope.launch {
try {
val flatPreset = getBuiltInPresets(currentBandMode).first()
repository.setEqualizerGains(currentProfile, flatPreset.bandGains, currentBandMode)
loadEqualizer()
} catch (e: Exception) {
DolbyConstants.dlog(TAG, "Error resetting gains: ${e.message}")
}
}
}
override fun onCleared() {
DolbyConstants.dlog(TAG, "ViewModel onCleared")
isCleared = true
viewModelScope.coroutineContext.cancelChildren()
profileChangeJob?.cancel()
profileChangeJob = null
repository.close()
super.onCleared()
}
companion object {
private const val TAG = "EqualizerViewModel"
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.utils
import android.content.Context
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
object HapticFeedbackHelper {
private val executor = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
enum class HapticIntensity(val value: Int) {
TEXTURE_TICK(1),
TICK(2),
CLICK(3),
DOUBLE_CLICK(4),
HEAVY_CLICK(5)
}
suspend fun triggerVibration(context: Context, intensity: HapticIntensity) {
withContext(executor) {
try {
val vibrator = getVibrator(context) ?: return@withContext
if (!vibrator.hasVibrator()) {
return@withContext
}
val effect = createVibrationEffect(intensity) ?: return@withContext
vibrator.cancel()
vibrator.vibrate(effect)
} catch (e: Exception) {
}
}
}
private fun getVibrator(context: Context): Vibrator? {
return try {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager
vibratorManager?.defaultVibrator
} catch (e: Exception) {
null
}
}
private fun createVibrationEffect(intensity: HapticIntensity): VibrationEffect? {
return try {
when (intensity) {
HapticIntensity.TEXTURE_TICK ->
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TEXTURE_TICK)
HapticIntensity.TICK ->
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
HapticIntensity.CLICK ->
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
HapticIntensity.DOUBLE_CLICK ->
VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK)
HapticIntensity.HEAVY_CLICK ->
VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK)
}
} catch (e: Exception) {
null
}
}
}
@Composable
fun rememberHapticFeedback(): HapticFeedback {
val context = LocalContext.current
return remember { HapticFeedback(context) }
}
class HapticFeedback(private val context: Context) {
suspend fun performHaptic(intensity: HapticFeedbackHelper.HapticIntensity) {
HapticFeedbackHelper.triggerVibration(context, intensity)
}
suspend fun click() = performHaptic(HapticFeedbackHelper.HapticIntensity.CLICK)
suspend fun doubleClick() = performHaptic(HapticFeedbackHelper.HapticIntensity.DOUBLE_CLICK)
suspend fun heavyClick() = performHaptic(HapticFeedbackHelper.HapticIntensity.HEAVY_CLICK)
suspend fun tick() = performHaptic(HapticFeedbackHelper.HapticIntensity.TICK)
suspend fun textureTick() = performHaptic(HapticFeedbackHelper.HapticIntensity.TEXTURE_TICK)
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024-2025 Lunaris AOSP
* SPDX-License-Identifier: Apache-2.0
*/
package org.lunaris.dolby.utils
import android.content.Context
import android.widget.Toast
object ToastHelper {
private var currentToast: Toast? = null
fun showToast(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT) {
currentToast?.cancel()
currentToast = Toast.makeText(context, message, duration)
currentToast?.show()
}
fun showLongToast(context: Context, message: String) {
showToast(context, message, Toast.LENGTH_LONG)
}
}

View File

@@ -1,11 +0,0 @@
<compatibility-matrix version="4.0" type="framework">
<hal format="hidl" optional="true">
<name>vendor.dolby.hardware.dms</name>
<transport>hwbinder</transport>
<version>2.0</version>
<interface>
<name>IDms</name>
<instance>default</instance>
</interface>
</hal>
</compatibility-matrix>

17
libshims/Android.bp Normal file
View File

@@ -0,0 +1,17 @@
//
// Copyright (C) 2023 The LineageOS Project
//
// SPDX-License-Identifier: Apache-2.0
//
cc_library_shared {
name: "libshim_dolby",
srcs: ["libshim_dolby.cpp"],
compile_multilib: "64",
shared_libs: [
"liblog",
"libdl",
"libc",
],
vendor: true,
}

View File

@@ -0,0 +1,27 @@
//
// SPDX-FileCopyrightText: The LineageOS Project
// SPDX-License-Identifier: Apache-2.0
//
#include <log/log.h>
#include <dlfcn.h>
extern "C" void _ZNK7android7RefBase9incStrongEPKv(void* thisptr, const void* id) {
if (!thisptr) {
ALOGE("DolbyShim: incStrong called on nullptr!");
return;
}
typedef void (*RealFunc)(void*, const void*);
static RealFunc real = (RealFunc)dlsym(RTLD_NEXT, "_ZNK7android7RefBase9incStrongEPKv");
if (real) real(thisptr, id);
}
extern "C" void _ZNK7android7RefBase9decStrongEPKv(void* thisptr, const void* id) {
if (!thisptr) {
ALOGE("DolbyShim: decStrong called on nullptr!");
return;
}
typedef void (*RealFunc)(void*, const void*);
static RealFunc real = (RealFunc)dlsym(RTLD_NEXT, "_ZNK7android7RefBase9decStrongEPKv");
if (real) real(thisptr, id);
}

View File

@@ -0,0 +1,4 @@
runtime_resource_overlay {
name: "DolbyFrameworksResCommon",
product_specific: true,
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.overlay.dolby">
<overlay
android:isStatic="true"
android:priority="300"
android:targetPackage="android" />
</manifest>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<!-- The default value for whether head tracking for
spatial audio is enabled for a newly connected audio device -->
<bool name="config_spatial_audio_head_tracking_enabled_default">true</bool>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<permissions>
<privapp-permissions package="com.dolby.daxappui">
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
<permission name="android.permission.MANAGE_USERS"/>
</privapp-permissions>
</permissions>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Copyright (C) 2018 Dolby Laboratories
* This program is protected under international and U.S. copyright laws as
* an unpublished work. This program is confidential and proprietary to the
* copyright owners. Reproduction or disclosure, in whole or in part, or the
* production of derivative works therefrom without the express permission of
* the copyright owners is prohibited.
* Copyright (C) 2018-2019 by Dolby Laboratories.
* All rights reserved.
-->
<permissions>
<privapp-permissions package="com.dolby.daxservice">
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
<permission name="android.permission.MANAGE_USERS"/>
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
<permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
</privapp-permissions>
</permissions>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<hidden-api-whitelisted-app package="com.dolby.daxappui"/>
<allow-in-power-save package="com.dolby.daxappui"/>
</config>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<hidden-api-whitelisted-app package="com.dolby.daxservice"/>
<allow-in-power-save package="com.dolby.daxservice"/>
</config>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<hidden-api-whitelisted-app package="com.dolby.daxservice"/>
</config>

BIN
proprietary/vendor/bin/hw/vendor.dolby.hardware.dms@2.0-service vendored Normal file → Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,7 @@
service vendor-dolby-media-c2-hal-1-0 /vendor/bin/hw/vendor.dolby.media.c2@1.0-service
class hal
user mediacodec
group camera mediadrm drmrpc
ioprio rt 4
writepid /dev/cpuset/foreground/tasks

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
proprietary/vendor/lib64/libdlbpreg.so vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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