Compare commits
151 Commits
vic
...
bka-aospa-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7062062d3a | ||
|
|
935738fd2f | ||
|
|
2653104313 | ||
|
|
6742a7510b | ||
|
|
814f200b96 | ||
|
|
e43d068c08 | ||
|
|
23e4a872b4 | ||
|
|
57a0f17b42 | ||
|
|
00f10ec799 | ||
|
|
5e8cd0042a | ||
|
|
11504da95e | ||
|
|
a0ef8bcdad | ||
|
|
bc9f70853b | ||
|
|
89e1f6d7fc | ||
|
|
08f2846211 | ||
|
|
14fd4056ad | ||
|
|
6920567e70 | ||
|
|
933f49f3ff | ||
|
|
868eefca14 | ||
|
|
b84590682a | ||
|
|
a329cb2b27 | ||
|
|
d054d08e26 | ||
|
|
278b17ead3 | ||
|
|
95281a58ab | ||
|
|
f64689fbde | ||
|
|
700575a500 | ||
|
|
96341f3d7c | ||
|
|
b906be38fa | ||
|
|
ecd925b89d | ||
|
|
9c334b7c91 | ||
|
|
6aebff870a | ||
|
|
eeb104fd9a | ||
|
|
c4c56687b2 | ||
|
|
0d664c26ee | ||
|
|
9481b3b8cf | ||
|
|
873fdd3f9a | ||
|
|
f2c74ae5bb | ||
|
|
28ec608d56 | ||
|
|
d0e960bcd5 | ||
|
|
a318b2211a | ||
|
|
c39a6aed6c | ||
|
|
267c2fac6f | ||
|
|
3aa7258551 | ||
|
|
ed4b480e2a | ||
|
|
be2b3ef445 | ||
|
|
9bba67c6bb | ||
|
|
5c91dcd7c9 | ||
|
|
0bdd102473 | ||
|
|
bc8daa97f6 | ||
|
|
3b85fb287e | ||
|
|
32b1e6aa87 | ||
|
|
c0e14ef659 | ||
|
|
336e88ed9c | ||
|
|
5db10db2b1 | ||
|
|
13c32ef7b4 | ||
|
|
0e03fffcdb | ||
|
|
fe94e80ffa | ||
|
|
944a468640 | ||
|
|
63bcd895df | ||
|
|
de23ccc185 | ||
|
|
088743af43 | ||
|
|
0353d1e21f | ||
|
|
fbb15cb28b | ||
|
|
67eed6a82c | ||
|
|
40fe6856d7 | ||
|
|
60ed7e0863 | ||
|
|
858790df5d | ||
|
|
31d7175068 | ||
|
|
d40e763990 | ||
|
|
4920046dbe | ||
|
|
c3f3a11aeb | ||
|
|
753b34ec59 | ||
|
|
05ca7c6837 | ||
|
|
a4cd52a83c | ||
|
|
b8e001e5eb | ||
|
|
440bb655d2 | ||
|
|
b27a259975 | ||
|
|
d43556f426 | ||
|
|
939088422f | ||
|
|
600851e820 | ||
|
|
ca33c4dd78 | ||
|
|
6db983af3e | ||
|
|
318f0ea617 | ||
|
|
b44187ef8f | ||
|
|
14efd187f8 | ||
|
|
4f1393f337 | ||
|
|
4774c82a5b | ||
|
|
7a1ef573d6 | ||
|
|
e92669bd12 | ||
|
|
586755d10b | ||
|
|
863fce3d24 | ||
|
|
5ab1d968be | ||
|
|
1d06c0bd9d | ||
|
|
708a47bda4 | ||
|
|
892caadbe7 | ||
|
|
e7dbf443a2 | ||
|
|
6b1bb6b1e8 | ||
|
|
0475f7ef37 | ||
|
|
0e837f8e94 | ||
|
|
263ba11385 | ||
|
|
54da022d03 | ||
|
|
4f911ddffa | ||
|
|
a4f8660263 | ||
|
|
187c0074e1 | ||
|
|
6b8b7bcae4 | ||
|
|
e2df31f5b8 | ||
|
|
10d3ac30f4 | ||
|
|
0ea2536cf8 | ||
|
|
8995c02d2d | ||
|
|
1c967d1859 | ||
|
|
82e51d2309 | ||
|
|
ee2f65b964 | ||
|
|
3c550064a0 | ||
|
|
56dc82a0a8 | ||
|
|
496446c564 | ||
|
|
a6202e7f90 | ||
|
|
00d74c76bc | ||
|
|
23662b74ac | ||
|
|
65d7336949 | ||
|
|
0befa6aa8c | ||
|
|
6d89341732 | ||
|
|
f5fb608d13 | ||
|
|
6cc95d2a09 | ||
|
|
048d3bfbe3 | ||
|
|
fac9089afd | ||
|
|
724f607cb8 | ||
|
|
da758a7873 | ||
|
|
a54c599e2b | ||
|
|
9c8b952778 | ||
|
|
9bce0718c3 | ||
|
|
4401819065 | ||
|
|
6fe60d867d | ||
|
|
beef2e28a1 | ||
|
|
efb871f672 | ||
|
|
1f2bc3d255 | ||
|
|
7870b8a0c7 | ||
|
|
6ad0f598e9 | ||
|
|
174e52ec51 | ||
|
|
881bb90ebe | ||
|
|
3fa57e3935 | ||
|
|
07b0a125fd | ||
|
|
fe709e060d | ||
|
|
e96e8c76fd | ||
|
|
31843fb4e7 | ||
|
|
f167291caf | ||
|
|
773b6cba9e | ||
|
|
04bec17707 | ||
|
|
a952ccb07b | ||
|
|
d2d27807cc | ||
|
|
667b4721be | ||
|
|
258557eec9 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vscode/
|
||||
309
Android.bp
309
Android.bp
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
48
README.mkdn
48
README.mkdn
@@ -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)
|
||||
|
||||
10
RemovePackagesDolby/Android.mk
Normal file
10
RemovePackagesDolby/Android.mk
Normal 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)
|
||||
20
configs/android.hardware.sensor.dynamic.head_tracker.xml
Normal file
20
configs/android.hardware.sensor.dynamic.head_tracker.xml
Normal 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
1480
configs/dax-default.xml
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
153
dolby.mk
@@ -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
45
dolby/Android.bp
Normal 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
101
dolby/AndroidManifest.xml
Normal 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>
|
||||
24
dolby/preinstalled-packages-platform-dolby.xml
Normal file
24
dolby/preinstalled-packages-platform-dolby.xml
Normal 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>
|
||||
8
dolby/res/drawable/ic_dolby_qs.xml
Normal file
8
dolby/res/drawable/ic_dolby_qs.xml
Normal 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>
|
||||
12
dolby/res/drawable/ic_launcher_background.xml
Normal file
12
dolby/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
6
dolby/res/drawable/ic_launcher_background__0.xml
Normal file
6
dolby/res/drawable/ic_launcher_background__0.xml
Normal 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>
|
||||
6
dolby/res/drawable/ic_launcher_background__1.xml
Normal file
6
dolby/res/drawable/ic_launcher_background__1.xml
Normal 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>
|
||||
8
dolby/res/drawable/ic_launcher_foreground.xml
Normal file
8
dolby/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
4
dolby/res/drawable/ic_launcher_mono.xml
Normal file
4
dolby/res/drawable/ic_launcher_mono.xml
Normal 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>
|
||||
7
dolby/res/mipmap-anydpi/ic_launcher.xml
Normal file
7
dolby/res/mipmap-anydpi/ic_launcher.xml
Normal 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
103
dolby/res/values/arrays.xml
Normal 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>
|
||||
4
dolby/res/values/colors.xml
Normal file
4
dolby/res/values/colors.xml
Normal 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>
|
||||
14
dolby/res/values/config.xml
Normal file
14
dolby/res/values/config.xml
Normal 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>
|
||||
16
dolby/res/values/integers.xml
Normal file
16
dolby/res/values/integers.xml
Normal 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>
|
||||
169
dolby/res/values/strings.xml
Normal file
169
dolby/res/values/strings.xml
Normal 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&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>
|
||||
9
dolby/res/values/themes.xml
Normal file
9
dolby/res/values/themes.xml
Normal 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>
|
||||
4
dolby/res/xml/file_paths.xml
Normal file
4
dolby/res/xml/file_paths.xml
Normal 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>
|
||||
121
dolby/src/org/lunaris/dolby/BootCompletedReceiver.kt
Normal file
121
dolby/src/org/lunaris/dolby/BootCompletedReceiver.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
53
dolby/src/org/lunaris/dolby/DolbyConstants.kt
Normal file
53
dolby/src/org/lunaris/dolby/DolbyConstants.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
134
dolby/src/org/lunaris/dolby/audio/DolbyAudioEffect.kt
Normal file
134
dolby/src/org/lunaris/dolby/audio/DolbyAudioEffect.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
74
dolby/src/org/lunaris/dolby/data/AppProfileManager.kt
Normal file
74
dolby/src/org/lunaris/dolby/data/AppProfileManager.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
819
dolby/src/org/lunaris/dolby/data/DolbyRepository.kt
Normal file
819
dolby/src/org/lunaris/dolby/data/DolbyRepository.kt
Normal 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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
234
dolby/src/org/lunaris/dolby/data/PresetExportManager.kt
Normal file
234
dolby/src/org/lunaris/dolby/data/PresetExportManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
79
dolby/src/org/lunaris/dolby/domain/models/Models.kt
Normal file
79
dolby/src/org/lunaris/dolby/domain/models/Models.kt
Normal 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()
|
||||
}
|
||||
77
dolby/src/org/lunaris/dolby/provider/SummaryProvider.kt
Normal file
77
dolby/src/org/lunaris/dolby/provider/SummaryProvider.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
288
dolby/src/org/lunaris/dolby/service/AppProfileMonitorService.kt
Normal file
288
dolby/src/org/lunaris/dolby/service/AppProfileMonitorService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
104
dolby/src/org/lunaris/dolby/service/DolbyNotificationListener.kt
Normal file
104
dolby/src/org/lunaris/dolby/service/DolbyNotificationListener.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
51
dolby/src/org/lunaris/dolby/tile/DolbyTileService.kt
Normal file
51
dolby/src/org/lunaris/dolby/tile/DolbyTileService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
164
dolby/src/org/lunaris/dolby/ui/DolbyActivity.kt
Normal file
164
dolby/src/org/lunaris/dolby/ui/DolbyActivity.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
386
dolby/src/org/lunaris/dolby/ui/components/CreditsDialog.kt
Normal file
386
dolby/src/org/lunaris/dolby/ui/components/CreditsDialog.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
232
dolby/src/org/lunaris/dolby/ui/components/EnhancedBottomNav.kt
Normal file
232
dolby/src/org/lunaris/dolby/ui/components/EnhancedBottomNav.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
711
dolby/src/org/lunaris/dolby/ui/components/ModernComponents.kt
Normal file
711
dolby/src/org/lunaris/dolby/ui/components/ModernComponents.kt
Normal 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
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
287
dolby/src/org/lunaris/dolby/ui/components/ProfileCarousel.kt
Normal file
287
dolby/src/org/lunaris/dolby/ui/components/ProfileCarousel.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
423
dolby/src/org/lunaris/dolby/ui/screens/AppProfileScreen.kt
Normal file
423
dolby/src/org/lunaris/dolby/ui/screens/AppProfileScreen.kt
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
1222
dolby/src/org/lunaris/dolby/ui/screens/ModernEqualizerScreen.kt
Normal file
1222
dolby/src/org/lunaris/dolby/ui/screens/ModernEqualizerScreen.kt
Normal file
File diff suppressed because it is too large
Load Diff
84
dolby/src/org/lunaris/dolby/ui/screens/Navigation.kt
Normal file
84
dolby/src/org/lunaris/dolby/ui/screens/Navigation.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
dolby/src/org/lunaris/dolby/ui/theme/Theme.kt
Normal file
161
dolby/src/org/lunaris/dolby/ui/theme/Theme.kt
Normal 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
|
||||
)
|
||||
}
|
||||
127
dolby/src/org/lunaris/dolby/ui/viewmodel/AppProfileViewModel.kt
Normal file
127
dolby/src/org/lunaris/dolby/ui/viewmodel/AppProfileViewModel.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
305
dolby/src/org/lunaris/dolby/ui/viewmodel/DolbyViewModel.kt
Normal file
305
dolby/src/org/lunaris/dolby/ui/viewmodel/DolbyViewModel.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
345
dolby/src/org/lunaris/dolby/ui/viewmodel/EqualizerViewModel.kt
Normal file
345
dolby/src/org/lunaris/dolby/ui/viewmodel/EqualizerViewModel.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
96
dolby/src/org/lunaris/dolby/utils/HapticFeedbackHelper.kt
Normal file
96
dolby/src/org/lunaris/dolby/utils/HapticFeedbackHelper.kt
Normal 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)
|
||||
}
|
||||
24
dolby/src/org/lunaris/dolby/utils/ToastHelper.kt
Normal file
24
dolby/src/org/lunaris/dolby/utils/ToastHelper.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
17
libshims/Android.bp
Normal 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,
|
||||
}
|
||||
27
libshims/libshim_dolby.cpp
Normal file
27
libshims/libshim_dolby.cpp
Normal 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);
|
||||
}
|
||||
4
overlay/DolbyFrameworksResCommon/Android.bp
Normal file
4
overlay/DolbyFrameworksResCommon/Android.bp
Normal file
@@ -0,0 +1,4 @@
|
||||
runtime_resource_overlay {
|
||||
name: "DolbyFrameworksResCommon",
|
||||
product_specific: true,
|
||||
}
|
||||
9
overlay/DolbyFrameworksResCommon/AndroidManifest.xml
Normal file
9
overlay/DolbyFrameworksResCommon/AndroidManifest.xml
Normal 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>
|
||||
10
overlay/DolbyFrameworksResCommon/res/values/config.xml
Normal file
10
overlay/DolbyFrameworksResCommon/res/values/config.xml
Normal 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>
|
||||
Binary file not shown.
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<config>
|
||||
<hidden-api-whitelisted-app package="com.dolby.daxservice"/>
|
||||
</config>
|
||||
Binary file not shown.
Binary file not shown.
BIN
proprietary/vendor/bin/hw/vendor.dolby.hardware.dms@2.0-service
vendored
Normal file → Executable file
BIN
proprietary/vendor/bin/hw/vendor.dolby.hardware.dms@2.0-service
vendored
Normal file → Executable file
Binary file not shown.
BIN
proprietary/vendor/bin/hw/vendor.dolby.media.c2@1.0-service
vendored
Executable file
BIN
proprietary/vendor/bin/hw/vendor.dolby.media.c2@1.0-service
vendored
Executable file
Binary file not shown.
7
proprietary/vendor/etc/init/vendor.dolby.media.c2@1.0-service.rc
vendored
Normal file
7
proprietary/vendor/etc/init/vendor.dolby.media.c2@1.0-service.rc
vendored
Normal 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
|
||||
|
||||
BIN
proprietary/vendor/lib/libdapparamstorage.so
vendored
BIN
proprietary/vendor/lib/libdapparamstorage.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/libdeccfg.so
vendored
BIN
proprietary/vendor/lib/libdeccfg.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/libqtigef.so
vendored
BIN
proprietary/vendor/lib/libqtigef.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/libstagefright_soft_ddpdec.so
vendored
BIN
proprietary/vendor/lib/libstagefright_soft_ddpdec.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/libstagefrightdolby.so
vendored
BIN
proprietary/vendor/lib/libstagefrightdolby.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/soundfx/libeffectproxy.so
vendored
BIN
proprietary/vendor/lib/soundfx/libeffectproxy.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/soundfx/libhwdap.so
vendored
BIN
proprietary/vendor/lib/soundfx/libhwdap.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/soundfx/libswdap.so
vendored
BIN
proprietary/vendor/lib/soundfx/libswdap.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib/soundfx/libswvqe.so
vendored
BIN
proprietary/vendor/lib/soundfx/libswvqe.so
vendored
Binary file not shown.
Binary file not shown.
BIN
proprietary/vendor/lib64/libcodec2_soft_ac4dec.so
vendored
Normal file
BIN
proprietary/vendor/lib64/libcodec2_soft_ac4dec.so
vendored
Normal file
Binary file not shown.
BIN
proprietary/vendor/lib64/libcodec2_soft_ddpdec.so
vendored
Normal file
BIN
proprietary/vendor/lib64/libcodec2_soft_ddpdec.so
vendored
Normal file
Binary file not shown.
BIN
proprietary/vendor/lib64/libcodec2_store_dolby.so
vendored
Normal file
BIN
proprietary/vendor/lib64/libcodec2_store_dolby.so
vendored
Normal file
Binary file not shown.
BIN
proprietary/vendor/lib64/libdapparamstorage.so
vendored
BIN
proprietary/vendor/lib64/libdapparamstorage.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/libdeccfg.so
vendored
BIN
proprietary/vendor/lib64/libdeccfg.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/libdlbdsservice.so
vendored
BIN
proprietary/vendor/lib64/libdlbdsservice.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/libdlbpreg.so
vendored
Normal file
BIN
proprietary/vendor/lib64/libdlbpreg.so
vendored
Normal file
Binary file not shown.
BIN
proprietary/vendor/lib64/libqtigef.so
vendored
BIN
proprietary/vendor/lib64/libqtigef.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/libspatializerparamstorage.so
vendored
Normal file
BIN
proprietary/vendor/lib64/libspatializerparamstorage.so
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
proprietary/vendor/lib64/libstagefrightdolby.so
vendored
BIN
proprietary/vendor/lib64/libstagefrightdolby.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/soundfx/libdlbvol.so
vendored
Normal file
BIN
proprietary/vendor/lib64/soundfx/libdlbvol.so
vendored
Normal file
Binary file not shown.
BIN
proprietary/vendor/lib64/soundfx/libeffectproxy.so
vendored
BIN
proprietary/vendor/lib64/soundfx/libeffectproxy.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/soundfx/libhwdap.so
vendored
BIN
proprietary/vendor/lib64/soundfx/libhwdap.so
vendored
Binary file not shown.
BIN
proprietary/vendor/lib64/soundfx/libswdap.so
vendored
BIN
proprietary/vendor/lib64/soundfx/libswdap.so
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user