28 Commits

Author SHA1 Message Date
pabloescobar-reborn
0420f3f245 configs: add dolby instance in FM 2025-05-19 04:43:07 +09:00
Dmitry
ea81a2acc2 dolby: add missing selinux policies 2025-05-13 21:26:09 +09:00
Ghosuto
fc013ffdef dolby: Update to use new tooltip position provider API 2025-05-03 19:17:11 +02:00
Bruno Martins
d69c31b038 dolby: Migrate MainSwitchBar and Switch to Material3
This follows Settings style updates for Android U.

Change-Id: Id6d3e7f193b6ef9fa6419d164265062328eb9170
2024-11-23 04:06:25 +05:30
Abhay Singh Gill
f479eef03b dolby: DolbyManager: Add custom profile
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-11-09 01:51:40 +05:30
Abhay Singh Gill
e8ebd395d1 dolby: DolbyManager: Add icons for dolby profiles
Also nuke voice profile and fixup some strings.

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-11-09 01:51:34 +05:30
Abhay Singh Gill
296d340b6d dolby: DolbyManager: Make bass enhancer available on speakers as well
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-11-06 14:52:00 +05:30
Sarthak Roy
41a93dd519 dolby: DolbyManager: Fix building in Android 15
Change-Id: Ie4e312bce0232c7a55ed2c29c8442f886f5aabd9
2024-11-06 14:52:00 +05:30
Adithya R
750b00415a dolby: DolbyManager: Add intelligent equalizer setting
Move preference-related classes to a new package while we're at it,
to reduce code clutter.
Also fix graphical equlizer

Change-Id: I2430e8ab9b6758503ce1777ec985a3e400b55b8e
2024-11-06 14:52:00 +05:30
Aditya R
a5e3d9c63d dolby: Switch to DolbyManager
Based on existing dirac implementation and observing stock
sound effects app and daxservice.

History -
- Allow bass enhancer to use in speaker also
- Remove deprecated PlainTooltipBox
- Fix build with kotlinc 1.9.0
- Add launcher icon
- Introduce graphical equalizer
- Do not set volume leveler amount
- Restore all settings upon bootup
- Rewrite in Kotlin
- Restore dolby profile on audio changes
- Guard debug logging
- Implement profile-specific Dolby settings
- Remove play/pause hack while toggling Dolby
- Set proper summary for dolby settings
- Always refresh playback if status changed
- Introduce Dolby Atmos

Co-authored-by: Henrique Silva <jhenrique09.mcz@hotmail.com>
Co-authored-by: Pranav Vashi <neobuddy89@gmail.com>
Co-authored-by: Fabian Leutenegger <fabian.leutenegger@bluewin.ch>
Co-authored-by: basamaryan <basam.aryan@gmail.com>
Co-authored-by: Ghosuto <clash.raja10@gmail.com>
2024-11-06 14:52:00 +05:30
Abhay Singh Gill
f3fc9e46e2 dolby: Update from sssi-user 14 67.1.A.2.287 1 release-keys
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-11-06 14:51:56 +05:30
Abhay Singh Gill
939e58c22a dolby: Remove redundant audio codec flag
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-11-03 01:28:02 +05:30
Abhay Singh Gill
4ec5eeda3c dolby: Remove OnePlus specific headphone tunings
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-11 07:08:41 +05:30
Abhay Singh Gill
b2e635a522 dolby: Fixup soong modules config
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-11 07:08:25 +05:30
Abhay Singh Gill
42005cbb91 dolby: Build codec2 required packages
Incase someone ignores the README.

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-10 19:48:50 +05:30
Abhay Singh Gill
3b0d7fd56d dolby: Update DaxUI
Now settings are restored after a reboot.

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-09 05:41:56 +05:30
Abhay Singh Gill
01a21f5098 dolby: Nuke libeffectproxy.so
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-09 04:06:34 +05:30
Abhay Singh Gill
4dc3303a4c Reapply "dolby: Link dolby blobs against v33 libstagefright_foundation"
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-09 04:06:13 +05:30
Abhay Singh Gill
3fb780dd60 dolby: Kang libswdap.so from https://github.com/sony-sm8550/proprietary_vendor_sony_extra
Else the effects do not update properly.

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-08 04:04:49 +05:30
Abhay Singh Gill
4be460186b dolby: Switch to sony dolby from pdx234
Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-08-08 02:32:28 +05:30
Abhay Singh Gill
d56e3b306b dolby: DaxUI: Override MusicFX and AudioFX 2024-03-19 05:58:35 +05:30
Abhay Singh Gill
5c1afecdb5 dolby: DaxUI: Fix clipped dialogue enhancer seekbar thumb 2024-03-19 03:02:51 +05:30
Fabian Leutenegger
8d0cda23b6 dolby: Link dolby blobs against v33 libstagefright_foundation
* fixes crashes and absurdly high CPU usage while using dolby
2024-03-15 16:05:38 +05:30
Abhay Singh Gill
3a6b0ad303 dolby: Remove unused blobs 2024-03-15 14:58:24 +05:30
Abhay Singh Gill
deb7018300 Revert "dolby: Update from OnePlus 8 (OOS-11)"
This reverts commit c527d9b162.
2024-03-15 14:56:55 +05:30
Abhay Singh Gill
c8c233e40d dolby: Add path variable 2024-03-15 14:01:44 +05:30
Abhay Singh Gill
5bd62626da dolby: Update readme 2024-03-15 14:01:41 +05:30
Abhay Singh Gill
f28d0a3718 dolby: DaxUI: Add more switches
For:-
• volume leveler
• surround virtualizer
• bass enhancer

Signed-off-by: Abhay Singh Gill <abhaygill017@gmail.com>
2024-03-15 13:49:23 +05:30
143 changed files with 3271 additions and 9913 deletions

1
.gitignore vendored
View File

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

View File

@@ -14,300 +14,3 @@
soong_namespace {
}
cc_prebuilt_library_shared {
name: "libdapparamstorage",
owner: "xiaomi",
strip: {
none: 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,
}
cc_prebuilt_library_shared {
name: "libdlbdsservice",
owner: "xiaomi",
strip: {
none: 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,
}

28
DolbyManager/Android.bp Normal file
View File

@@ -0,0 +1,28 @@
//
// Copyright (C) 2017-2021 The LineageOS Project
// (C) 2023-24 Paranoid Android
//
// SPDX-License-Identifier: Apache-2.0
//
android_app {
name: "DolbyManager",
srcs: ["src/**/*.kt"],
resource_dirs: ["res"],
certificate: "platform",
platform_apis: true,
system_ext_specific: true,
privileged: true,
overrides: ["MusicFX", "AudioFX"],
static_libs: [
"SettingsLib",
"SpaLib",
"androidx.activity_activity-compose",
"androidx.compose.material3_material3",
"androidx.compose.runtime_runtime",
"androidx.preference_preference",
"com.google.android.material_material",
],
}

View File

@@ -1,40 +1,32 @@
<?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"
package="co.aospa.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">
android:persistent="true">
<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:name=".DolbyActivity"
android:label="@string/dolby_title"
android:theme="@style/Theme.SubSettingsBase"
android:exported="true">
<intent-filter>
<action android:name="com.android.settings.action.IA_SETTINGS" />
@@ -52,21 +44,22 @@
<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" />
android:value="content://co.aospa.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>
<activity
android:name=".geq.EqualizerActivity"
android:label="@string/dolby_preset"
android:theme="@style/Theme.SubSettingsBase"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service
android:name=".tile.DolbyTileService"
android:name=".DolbyTileService"
android:icon="@drawable/ic_dolby_qs"
android:label="@string/dolby_title"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -77,25 +70,10 @@
</service>
<provider
android:name=".provider.SummaryProvider"
android:authorities="org.lunaris.dolby.summary">
android:name=".SummaryProvider"
android:authorities="co.aospa.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>
</manifest>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?android:attr/colorControlNormal">
<path android:fillColor="#000000" 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="#000000" android:pathData="M23,4.0924L23,18.8825C19.4973,19.298 16.399,18.6968 14.3366,15.6947C12.5148,13.043 12.4594,10.2265 14.2129,7.5241C16.244,4.394 19.3953,3.7204 23,4.0924" android:strokeColor="#00000000" android:strokeWidth="1" android:fillType="evenOdd"/>
</group>
</vector>

View File

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

View File

@@ -0,0 +1,24 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#000000"
android:pathData="M 69.584 66.547 L 63.792 70.797 L 38.646 71.697 L 13.5 72.597 L 13.5 89.723 C 13.5 99.143 13.865 111.999 14.313 118.292 L 15.124 129.734 L 178.5 129.734 L 178.5 102.234 L 121.248 102.234 L 99.203 82.234 C 87.078 71.234 76.756 62.248 76.266 62.266 C 75.776 62.282 72.769 64.209 69.584 66.547"
android:strokeWidth="1.25"/>
<path
android:fillColor="#000000"
android:pathData="M 69.125 67.065 L 62.875 71.921 L 38.188 71.99 L 13.5 72.06 L 13.504 81.122 C 13.506 86.106 13.876 99.044 14.325 109.872 L 15.141 129.56 L 178.5 129.56 L 178.5 103.31 L 122.126 103.31 L 99.675 82.997 C 87.326 71.826 76.808 62.577 76.299 62.446 C 75.791 62.315 72.563 64.394 69.125 67.065"
android:strokeWidth="1.25"/>
<path
android:fillColor="#000000"
android:pathData="M 69.854 67.249 L 63.237 72.187 L 13.311 72.187 L 14.119 84.374 C 14.562 91.077 14.929 104.016 14.932 113.124 L 14.939 129.687 L 178.689 129.687 L 178.689 103.437 L 150.251 103.428 L 121.814 103.418 L 99.143 82.865 L 76.471 62.313 L 69.854 67.249"
android:strokeWidth="1.25"/>
<path
android:fillColor="#000000"
android:pathData="M 69.141 67.266 L 63.221 71.609 L 45.888 71.609 C 36.354 71.609 25.238 71.956 21.184 72.378 L 13.813 73.147 L 14.115 93.316 C 14.281 104.408 14.748 117.001 15.151 121.297 L 15.886 129.109 L 178.188 129.109 L 178.188 102.859 L 120.703 102.859 L 98.696 82.859 C 86.594 71.859 76.324 62.873 75.876 62.891 C 75.429 62.907 72.399 64.876 69.141 67.266"
android:strokeWidth="1.25"/>
</vector>

View File

@@ -0,0 +1,24 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#000000"
android:pathData="M 126.625 63.749 C 120.886 65.812 115.12 69.774 103.5 79.643 C 85.566 94.873 77.207 98.716 62.179 98.637 C 51.344 98.582 42.841 96.091 34.946 90.659 C 29.282 86.763 22.25 77.097 22.25 73.209 C 22.25 71.466 21.259 70.954 17.875 70.954 L 13.5 70.954 L 13.5 129.704 L 178.5 129.704 L 178.5 86.329 L 174.915 81.381 C 169.567 73.999 161.404 67.534 153.865 64.714 C 146.447 61.939 132.988 61.463 126.625 63.749"
android:strokeWidth="1.25"/>
<path
android:fillColor="#000000"
android:pathData="M 128.493 63.479 C 122.229 65.384 117.026 68.819 104.59 79.26 C 89.19 92.189 83.945 95.359 73.54 98.024 C 60.438 101.381 44.281 98.268 33.966 90.4 C 28.19 85.994 22.25 77.679 22.25 73.999 C 22.25 71.475 21.634 71.061 17.875 71.061 L 13.5 71.061 L 13.5 129.811 L 178.5 129.811 L 178.494 108.249 L 178.488 86.686 L 173.807 80.541 C 163.119 66.51 143.193 59.008 128.493 63.479"
android:strokeWidth="1.25"/>
<path
android:fillColor="#000000"
android:pathData="M 129.75 63.348 C 122.865 65.451 115.829 70.014 103.442 80.413 C 96.136 86.547 86.969 92.947 82.48 95.048 C 60.304 105.426 32.044 97.046 23.267 77.492 C 21.242 72.979 20.328 72.179 17.191 72.179 L 13.5 72.179 L 13.5 129.679 L 178.5 129.679 L 178.456 108.742 C 178.41 86.981 178.329 86.587 172.313 78.669 C 163.463 67.022 142.469 59.466 129.75 63.348"
android:strokeWidth="1.25"/>
<path
android:fillColor="#000000"
android:pathData="M 131.184 63.546 C 123.825 64.992 117.559 68.919 103.5 80.894 C 96.969 86.458 87.98 92.824 83.526 95.043 C 75.941 98.821 74.591 99.077 62.25 99.077 C 49.875 99.077 48.564 98.827 40.714 94.962 C 31.655 90.503 24.469 83.298 22.19 76.396 C 21.04 72.909 20.19 72.202 17.152 72.202 L 13.5 72.202 L 13.5 129.702 L 178.5 129.702 L 178.5 87.577 L 174.965 82.702 C 170.49 76.532 163.525 70.403 157.488 67.323 C 153.545 65.312 139.684 61.884 137.333 62.339 C 136.944 62.414 134.176 62.957 131.184 63.546"
android:strokeWidth="1.25"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#000000"
android:pathData="M 13.5 96 L 13.5 103.5 L 178.5 103.5 L 178.5 88.5 L 13.5 88.5 L 13.5 96"
android:strokeWidth="1.25"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,28 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#000000"
android:pathData="M 48.789 64.919 C 45.865 66.366 40.885 70.429 37.721 73.948 C 28.465 84.243 13.479 116.446 13.479 126.041 L 13.479 129.786 L 95.979 129.786 C 141.354 129.786 178.479 129.486 178.479 129.12 C 178.479 128.753 177.131 125.8 175.483 122.558 C 170.413 112.579 162.723 104.006 155.705 100.509 C 149.673 97.503 148.053 97.286 131.582 97.286 C 105.944 97.286 102.89 95.881 86.335 76.454 C 74.977 63.124 61.056 58.848 48.789 64.919"
android:strokeWidth="1.25"
android:fillType="evenOdd"/>
<path
android:fillColor="#000000"
android:pathData="M 49.712 64.871 C 40.942 68.842 33.167 78.157 25.45 93.941 C 18.063 109.047 13.462 121.585 13.462 126.604 L 13.462 129.769 L 178.496 129.769 L 176.673 125.407 C 171.895 113.971 159.81 101.317 151.203 98.739 C 148.476 97.921 138.591 97.264 128.942 97.256 C 107.671 97.241 102.106 95.265 93.347 84.617 C 76.128 63.684 64.227 58.297 49.712 64.871"
android:strokeWidth="1.25"
android:fillType="evenOdd"/>
<path
android:fillColor="#000000"
android:pathData="M 52.781 63.452 C 47.321 65.482 40.256 70.907 35.792 76.496 C 27.971 86.29 13.406 119.09 13.406 126.91 L 13.406 129.502 L 178.552 129.502 L 174.586 122.06 C 169.31 112.162 163.213 105.23 156.113 101.056 C 150.453 97.729 149.705 97.625 130.807 97.53 C 107.81 97.414 103.646 96.04 94.012 85.387 C 80.912 70.9 77.898 68.045 73.171 65.632 C 67.157 62.565 57.846 61.569 52.781 63.452"
android:strokeWidth="1.25"
android:fillType="evenOdd"/>
<path
android:fillColor="#000000"
android:pathData="M 56.078 63.069 C 44.607 65.852 35.711 75.314 26.246 94.802 C 19.707 108.263 16.537 116.731 14.817 125.334 L 14.005 129.397 L 95.98 129.397 C 165.857 129.397 177.953 129.136 177.953 127.627 C 177.953 124.146 165.22 106.857 160.048 103.314 C 152.515 98.156 150.572 97.791 129.828 97.641 C 111.478 97.507 110.935 97.424 104.33 93.776 C 99.905 91.331 95.316 87.208 91 81.798 C 80.578 68.737 74.351 64.324 64.203 62.812 C 61.797 62.453 58.141 62.568 56.078 63.069"
android:strokeWidth="1.25"
android:fillType="evenOdd"/>
</vector>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?android:attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="72.0"
android:viewportHeight="72.0">
<path
android:fillColor="#FF000000"
android:pathData="M45,9L45,27L51,27L51,21L63,21L63,15L51,15L51,9L45,9M9,15L9,21L39,21L39,15L9,15M21,27L21,33L9,33L9,39L21,39L21,45L27,45L27,27L21,27M33,33L33,39L63,39L63,33L33,33M33,45L33,63L39,63L39,57L63,57L63,51L39,51L39,45L33,45M9,51L9,57L27,57L27,51L9,51z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?android:attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="72.0"
android:viewportHeight="72.0">
<path
android:fillColor="#FF000000"
android:pathData="M21,6L21,39L30,39L30,66C34.0185,62.1036 36.3516,56.7839 39.2006,52C43.3436,45.0435 48.7076,37.7933 51,30L40,30C43.3656,21.916 48.5711,14.4315 51,6L21,6z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?android:attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="72.0"
android:viewportHeight="72.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.84,13.82C9.62,11.74 12.52,12.02 14.98,11.95C16.97,15.97 19,19.98 21,24C24,24 27,24 30,24C28,20 26,16 24,12C26,12 28,12 30,12C32,16 34,20 36,24C39,24 42,24 45,24C43,20 41,16 39,12C41,12 43,12 45,12C47,16 49,20 51,24C54,24 57,24 60,24C58,20 56,16 54,12C58,12 62,12 66,12C65.99,25.69 66.01,39.38 66,53.07C66.42,56.49 63.62,59.98 60.12,59.96C44.39,60.06 28.66,59.97 12.93,60C9.52,60.43 6.02,57.62 6.04,54.12C5.93,42.75 6.06,31.37 6,19.99C5.95,17.82 6.08,15.36 7.84,13.82Z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?android:attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="72.0"
android:viewportHeight="72.0">
<path
android:fillColor="#FF000000"
android:pathData="M36,9L36,40C32.245,38.8901 27.6726,38.2912 24.0154,40.179C17.5734,43.5044 15.8784,53.3215 20.4336,58.8912C25.822,65.4796 38.0937,64.1632 41.2577,55.9961C42.655,52.3894 42,47.7931 42,44L42,21L54,21L54,9L36,9z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?android:attr/colorControlNormal"
android:pathData="M520,630L520,570L680,570L680,630L520,630ZM580,840L580,790L520,790L520,730L580,730L580,680L640,680L640,840L580,840ZM680,790L680,730L840,730L840,790L680,790ZM720,680L720,520L780,520L780,570L840,570L840,630L780,630L780,680L720,680ZM831,400L748,400Q722,312 649,256Q576,200 480,200Q363,200 281.5,281.5Q200,363 200,480Q200,552 232.5,612Q265,672 320,710L320,600L400,600L400,840L160,840L160,760L254,760Q192,710 156,637.5Q120,565 120,480Q120,405 148.5,339.5Q177,274 225.5,225.5Q274,177 339.5,148.5Q405,120 480,120Q609,120 706.5,199.5Q804,279 831,400Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?android:attr/colorControlNormal"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L680,120L840,280L840,492Q821,484 800.5,481.5Q780,479 760,482L760,313L647,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L440,760L440,764L440,840L200,840ZM200,200L200,313L200,482Q200,485 200,494.5Q200,504 200,519L200,760L200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200ZM520,920L520,797L741,577Q750,568 761,564Q772,560 783,560Q795,560 806,564.5Q817,569 826,578L863,615Q871,624 875.5,635Q880,646 880,657Q880,668 876,679.5Q872,691 863,700L643,920L520,920ZM820,657L820,657L783,620L783,620L820,657ZM580,860L618,860L739,738L721,719L702,701L580,822L580,860ZM721,719L702,701L702,701L739,738L739,738L721,719ZM240,400L600,400L600,240L240,240L240,400ZM480,720Q481,720 482,720Q483,720 484,720L600,605Q600,603 600,602.5Q600,602 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720Z"/>
</vector>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="@dimen/settingslib_switchbar_margin"
android:paddingBottom="@dimen/settingslib_switchbar_margin"
android:orientation="vertical">
<LinearLayout
android:id="@+id/frame"
android:minHeight="@dimen/settingslib_min_switch_bar_height"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingStart="@dimen/settingslib_switchbar_padding_left"
android:paddingEnd="@dimen/settingslib_switchbar_padding_right"
android:background="@drawable/settingslib_switch_bar_bg">
<TextView
android:id="@+id/switch_text"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginEnd="@dimen/settingslib_switch_title_margin"
android:layout_marginVertical="@dimen/settingslib_switch_title_margin"
android:layout_gravity="center_vertical"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceListItem"
android:hyphenationFrequency="normalFast"
android:lineBreakWordStyle="phrase"
style="@style/MainSwitchText.Settingslib" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@android:id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:theme="@style/Theme.Material3.DynamicColors.DayNight" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2021 The Android Open Source Project
(C) 2024 Paranoid Android
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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="56dp"
android:gravity="end|center_vertical"
android:orientation="horizontal"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:paddingRight="0dp"
android:paddingEnd="0dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<androidx.preference.internal.PreferenceImageView
android:id="@+id/ieq_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:maxWidth="56dp"
app:maxHeight="56dp"/>
</LinearLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<com.google.android.material.materialswitch.MaterialSwitch
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:theme="@style/Theme.Material3.DynamicColors.DayNight" />

View File

@@ -0,0 +1,98 @@
<?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_movie</item>
<item>@string/dolby_profile_music</item>
<item>@string/dolby_profile_custom</item>
</string-array>
<string-array name="dolby_profile_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</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>
</string-array>
<string-array name="dolby_preset_values">
<!--
<item>0,0,0,0,0,0,0,0,0,0</item>
<item>4,1,-2,-0.25,0,-2,0,-2,0.5,4</item>
<item>0,0,0,-1,-1,-3,-0.5,0,0,0</item>
<item>-2,-0.5,-5,-1,0,0,-0.5,-3,-0.5,0</item>
<item>0,0,0,0,0.5,3,1,6,2,6</item>
<item>3,0,-3,-0.5,-0.5,-3,-0.5,0,0,2</item>
<item>2,2,-6,-2,3,1,0,1,0,2</item>
<item>3,1,-1,0,-0.5,-3,-0.5,0,0,0</item>
<item>2,0,0,-1.25,-1,-4,0,0,0,0</item>
-->
<item>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</item>
<item>60,36,12,-12,-36,-24,-12,-8,-4,-20,-36,-20,-4,-20,-36,-16,4,32,60,60</item>
<item>8,8,8,8,8,0,-8,-8,-8,-24,-40,-20,0,4,8,8,8,8,8,8</item>
<item>-13,-1,11,-25,-61,-29,3,11,19,19,19,15,11,-9,-29,-9,11,15,19,19</item>
<item>-32,-32,-32,-32,-32,-32,-32,-28,-24,-4,16,0,-16,24,64,32,0,32,64,64</item>
<item>52,28,4,-20,-44,-24,-4,-4,-4,-24,-44,-24,-4,0,4,4,4,20,36,36</item>
<item>28,28,28,-36,-100,-68,-36,4,44,28,12,4,-4,4,12,4,-4,12,28,28</item>
<item>50,34,18,2,-14,-6,2,-2,-6,-26,-46,-26,-6,-2,2,2,2,2,2,2</item>
<item>40,24,8,8,8,-4,-16,-12,-8,-32,-56,-24,8,8,8,8,8,8,8,8</item>
</string-array>
<string-array name="dolby_dialogue_entries">
<item>@string/dolby_off</item>
<item>@string/dolby_low</item>
<item>@string/dolby_medium</item>
<item>@string/dolby_high</item>
<item>@string/dolby_max</item>
</string-array>
<string-array name="dolby_dialogue_values">
<item>0</item>
<item>2</item>
<item>6</item>
<item>9</item>
<item>12</item>
</string-array>
<string-array name="dolby_stereo_entries">
<item>@string/dolby_low</item>
<item>@string/dolby_high</item>
</string-array>
<string-array name="dolby_stereo_values">
<item>4</item>
<item>40</item>
</string-array>
<string-array name="dolby_ieq_entries">
<item>@string/dolby_off</item>
<item>@string/dolby_balanced</item>
<item>@string/dolby_warm</item>
<item>@string/dolby_detailed</item>
</string-array>
<string-array name="dolby_ieq_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
</resources>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023-24 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">Graphic 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">Settings</string>
<string name="dolby_bass_enhancer">Bass enhancer</string>
<string name="dolby_dialogue_enhancer">Dialogue enhancer</string>
<string name="dolby_spk_virtualizer">Speaker virtualization</string>
<string name="dolby_hp_virtualizer">Headphone virtualization</string>
<string name="dolby_stereo_widening">Stereo widening</string>
<string name="dolby_volume_leveler">Volume leveler</string>
<string name="dolby_connect_headphones">Connect headphones</string>
<string name="dolby_reset_profile">Reset to defaults</string>
<string name="dolby_reset_profile_toast">Successfully reset settings for %1$s profile</string>
<!-- Dolby profiles -->
<string name="dolby_profile_dynamic">Dynamic</string>
<string name="dolby_profile_movie">Movie</string>
<string name="dolby_profile_music">Music</string>
<string name="dolby_profile_custom">Custom</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>
<!-- 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>
<!-- 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>
</resources>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023-24 Paranoid Android
SPDX-License-Identifier: Apache-2.0
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/dolby_title">
<com.android.settingslib.widget.MainSwitchPreference
android:defaultValue="true"
android:key="dolby_enable"
android:title="@string/dolby_enable" />
<ListPreference
android:key="dolby_profile"
android:entries="@array/dolby_profile_entries"
android:entryValues="@array/dolby_profile_values"
android:defaultValue="0"
android:title="@string/dolby_profile_title"
android:summary="%s" />
<PreferenceCategory
android:title="@string/dolby_category_settings">
<Preference
android:key="dolby_preset"
android:title="@string/dolby_preset">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="co.aospa.dolby"
android:targetClass="co.aospa.dolby.geq.EqualizerActivity" />
</Preference>
<co.aospa.dolby.preference.DolbyIeqPreference
android:key="dolby_ieq"
android:entries="@array/dolby_ieq_entries"
android:entryValues="@array/dolby_ieq_values"
android:title="@string/dolby_ieq" />
<SwitchPreferenceCompat
android:key="dolby_spk_virtualizer"
android:title="@string/dolby_spk_virtualizer" />
<SwitchPreferenceCompat
android:key="dolby_virtualizer"
android:title="@string/dolby_hp_virtualizer" />
<ListPreference
android:key="dolby_stereo"
android:entries="@array/dolby_stereo_entries"
android:entryValues="@array/dolby_stereo_values"
android:title="@string/dolby_stereo_widening"
android:dependency="dolby_virtualizer" />
<ListPreference
android:key="dolby_dialogue"
android:entries="@array/dolby_dialogue_entries"
android:entryValues="@array/dolby_dialogue_values"
android:title="@string/dolby_dialogue_enhancer" />
<SwitchPreferenceCompat
android:key="dolby_bass"
android:title="@string/dolby_bass_enhancer" />
<SwitchPreferenceCompat
android:key="dolby_volume"
android:title="@string/dolby_volume_leveler" />
<Preference
android:key="dolby_reset"
android:title="@string/dolby_reset_profile" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
private const val TAG = "Dolby-Boot"
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent: ${intent.action}")
if (intent.action != Intent.ACTION_BOOT_COMPLETED) {
return
}
Log.i(TAG, "Boot completed, starting dolby")
DolbyController.getInstance(context).onBootCompleted()
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.os.Bundle
import co.aospa.dolby.preference.DolbySettingsFragment
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
private const val TAG = "DolbyActivity"
class DolbyActivity : CollapsingToolbarBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fragmentManager.beginTransaction()
.replace(com.android.settingslib.collapsingtoolbar.R.id.content_frame, DolbySettingsFragment(), TAG)
.commit()
}
}

View File

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

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.util.Log
class DolbyConstants {
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 {
return "${name}(${id})"
}
}
companion object {
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 = "dolby_stereo"
const val PREF_DIALOGUE = "dolby_dialogue"
const val PREF_BASS = "dolby_bass"
const val PREF_VOLUME = "dolby_volume"
const val PREF_RESET = "dolby_reset"
val PROFILE_SPECIFIC_PREFS = setOf(
PREF_PRESET,
PREF_IEQ,
PREF_HP_VIRTUALIZER,
PREF_SPK_VIRTUALIZER,
PREF_STEREO,
PREF_DIALOGUE,
PREF_BASS,
PREF_VOLUME
)
fun dlog(tag: String, msg: String) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(tag, msg)
}
}
}
}

View File

@@ -0,0 +1,316 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.AudioPlaybackCallback
import android.media.AudioPlaybackConfiguration
import android.os.Handler
import android.util.Log
import androidx.preference.PreferenceManager
import co.aospa.dolby.DolbyConstants.Companion.dlog
import co.aospa.dolby.DolbyConstants.DsParam
import co.aospa.dolby.R
internal class DolbyController private constructor(
private val context: Context
) {
private var dolbyEffect = DolbyAudioEffect(EFFECT_PRIORITY, audioSession = 0)
private val audioManager = context.getSystemService(AudioManager::class.java)
private val handler = Handler(context.mainLooper)
// Restore current profile on every media session
private val playbackCallback = object : AudioPlaybackCallback() {
override fun onPlaybackConfigChanged(configs: List<AudioPlaybackConfiguration>) {
val isPlaying = configs.any {
it.playerState == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
}
dlog(TAG, "onPlaybackConfigChanged: isPlaying=$isPlaying")
if (isPlaying)
setCurrentProfile()
}
}
// Restore current profile on audio device change
private val audioDeviceCallback = object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
dlog(TAG, "onAudioDevicesAdded")
setCurrentProfile()
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
dlog(TAG, "onAudioDevicesRemoved")
setCurrentProfile()
}
}
private var registerCallbacks = false
set(value) {
if (field == value) return
field = value
dlog(TAG, "setRegisterCallbacks($value)")
if (value) {
audioManager!!.registerAudioPlaybackCallback(playbackCallback, handler)
audioManager.registerAudioDeviceCallback(audioDeviceCallback, handler)
} else {
audioManager!!.unregisterAudioPlaybackCallback(playbackCallback)
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
}
}
var dsOn: Boolean
get() =
dolbyEffect.dsOn.also {
dlog(TAG, "getDsOn: $it")
}
set(value) {
dlog(TAG, "setDsOn: $value")
checkEffect()
dolbyEffect.dsOn = value
registerCallbacks = value
if (value)
setCurrentProfile()
}
var profile: Int
get() =
dolbyEffect.profile.also {
dlog(TAG, "getProfile: $it")
}
set(value) {
dlog(TAG, "setProfile: $value")
checkEffect()
dolbyEffect.profile = value
}
init {
dlog(TAG, "initialized")
}
fun onBootCompleted() {
dlog(TAG, "onBootCompleted")
// Restore our main settings
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
dsOn = prefs.getBoolean(DolbyConstants.PREF_ENABLE, true)
context.resources.getStringArray(R.array.dolby_profile_values)
.map { it.toInt() }
.forEach { profile ->
// Reset dolby first to prevent it from loading bad settings
dolbyEffect.resetProfileSpecificSettings(profile)
// Now restore our profile-specific settings
restoreSettings(profile)
}
// Finally restore the current profile.
setCurrentProfile()
}
private fun restoreSettings(profile: Int) {
dlog(TAG, "restoreSettings(profile=$profile)")
val prefs = context.getSharedPreferences("profile_$profile", Context.MODE_PRIVATE)
setPreset(
prefs.getString(DolbyConstants.PREF_PRESET, getPreset(profile))!!,
profile
)
setIeqPreset(
prefs.getString(
DolbyConstants.PREF_IEQ,
getIeqPreset(profile).toString()
)!!.toInt(),
profile
)
setHeadphoneVirtEnabled(
prefs.getBoolean(DolbyConstants.PREF_HP_VIRTUALIZER, getHeadphoneVirtEnabled(profile)),
profile
)
setSpeakerVirtEnabled(
prefs.getBoolean(DolbyConstants.PREF_SPK_VIRTUALIZER, getSpeakerVirtEnabled(profile)),
profile
)
setStereoWideningAmount(
prefs.getString(
DolbyConstants.PREF_STEREO,
getStereoWideningAmount(profile).toString()
)!!.toInt(),
profile
)
setDialogueEnhancerAmount(
prefs.getString(
DolbyConstants.PREF_DIALOGUE,
getDialogueEnhancerAmount(profile).toString()
)!!.toInt(),
profile
)
setBassEnhancerEnabled(
prefs.getBoolean(DolbyConstants.PREF_BASS, getBassEnhancerEnabled(profile)),
profile
)
setVolumeLevelerEnabled(
prefs.getBoolean(DolbyConstants.PREF_VOLUME, getVolumeLevelerEnabled(profile)),
profile
)
}
private fun checkEffect() {
if (!dolbyEffect.hasControl()) {
Log.w(TAG, "lost control, recreating effect")
dolbyEffect.release()
dolbyEffect = DolbyAudioEffect(EFFECT_PRIORITY, audioSession = 0)
}
}
private fun setCurrentProfile() {
dlog(TAG, "setCurrentProfile")
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
profile = prefs.getString(DolbyConstants.PREF_PROFILE, "0" /*dynamic*/)!!.toInt()
}
fun getProfileName(): String? {
val profile = dolbyEffect.profile.toString()
val profiles = context.resources.getStringArray(R.array.dolby_profile_values)
val profileIndex = profiles.indexOf(profile)
dlog(TAG, "getProfileName: profile=$profile index=$profileIndex")
return if (profileIndex == -1) null else context.resources.getStringArray(
R.array.dolby_profile_entries
)[profileIndex]
}
fun resetProfileSpecificSettings() {
dlog(TAG, "resetProfileSpecificSettings")
checkEffect()
dolbyEffect.resetProfileSpecificSettings()
context.deleteSharedPreferences("profile_$profile")
}
fun getPreset(profile: Int = this.profile): String {
val gains = dolbyEffect.getDapParameter(DsParam.GEQ_BAND_GAINS, profile)
return gains.joinToString(separator = ",").also {
dlog(TAG, "getPreset: $it")
}
}
fun setPreset(value: String, profile: Int = this.profile) {
dlog(TAG, "setPreset: $value")
checkEffect()
val gains = value.split(",")
.map { it.toInt() }
.toIntArray()
dolbyEffect.setDapParameter(DsParam.GEQ_BAND_GAINS, gains, profile)
}
fun getPresetName(): String {
val presets = context.resources.getStringArray(R.array.dolby_preset_values)
val presetIndex = presets.indexOf(getPreset())
return if (presetIndex == -1) {
"Custom"
} else {
context.resources.getStringArray(
R.array.dolby_preset_entries
)[presetIndex]
}
}
fun getHeadphoneVirtEnabled(profile: Int = this.profile) =
dolbyEffect.getDapParameterBool(DsParam.HEADPHONE_VIRTUALIZER, profile).also {
dlog(TAG, "getHeadphoneVirtEnabled: $it")
}
fun setHeadphoneVirtEnabled(value: Boolean, profile: Int = this.profile) {
dlog(TAG, "setHeadphoneVirtEnabled: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.HEADPHONE_VIRTUALIZER, value, profile)
}
fun getSpeakerVirtEnabled(profile: Int = this.profile) =
dolbyEffect.getDapParameterBool(DsParam.SPEAKER_VIRTUALIZER, profile).also {
dlog(TAG, "getSpeakerVirtEnabled: $it")
}
fun setSpeakerVirtEnabled(value: Boolean, profile: Int = this.profile) {
dlog(TAG, "setSpeakerVirtEnabled: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.SPEAKER_VIRTUALIZER, value, profile)
}
fun getBassEnhancerEnabled(profile: Int = this.profile) =
dolbyEffect.getDapParameterBool(DsParam.BASS_ENHANCER_ENABLE, profile).also {
dlog(TAG, "getBassEnhancerEnabled: $it")
}
fun setBassEnhancerEnabled(value: Boolean, profile: Int = this.profile) {
dlog(TAG, "setBassEnhancerEnabled: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.BASS_ENHANCER_ENABLE, value, profile)
}
fun getVolumeLevelerEnabled(profile: Int = this.profile) =
dolbyEffect.getDapParameterBool(DsParam.VOLUME_LEVELER_ENABLE, profile).also {
dlog(TAG, "getVolumeLevelerEnabled: $it")
}
fun setVolumeLevelerEnabled(value: Boolean, profile: Int = this.profile) {
dlog(TAG, "setVolumeLevelerEnabled: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.VOLUME_LEVELER_ENABLE, value, profile)
}
fun getStereoWideningAmount(profile: Int = this.profile) =
dolbyEffect.getDapParameterInt(DsParam.STEREO_WIDENING_AMOUNT, profile).also {
dlog(TAG, "getStereoWideningAmount: $it")
}
fun setStereoWideningAmount(value: Int, profile: Int = this.profile) {
dlog(TAG, "setStereoWideningAmount: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.STEREO_WIDENING_AMOUNT, value, profile)
}
fun getDialogueEnhancerAmount(profile: Int = this.profile): Int {
val enabled = dolbyEffect.getDapParameterBool(DsParam.DIALOGUE_ENHANCER_ENABLE, profile)
val amount = if (enabled) {
dolbyEffect.getDapParameterInt(DsParam.DIALOGUE_ENHANCER_AMOUNT, profile)
} else 0
dlog(TAG, "getDialogueEnhancerAmount: enabled=$enabled amount=$amount")
return amount
}
fun setDialogueEnhancerAmount(value: Int, profile: Int = this.profile) {
dlog(TAG, "setDialogueEnhancerAmount: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.DIALOGUE_ENHANCER_ENABLE, (value > 0), profile)
dolbyEffect.setDapParameter(DsParam.DIALOGUE_ENHANCER_AMOUNT, value, profile)
}
fun getIeqPreset(profile: Int = this.profile) =
dolbyEffect.getDapParameterInt(DsParam.IEQ_PRESET, profile).also {
dlog(TAG, "getIeqPreset: $it")
}
fun setIeqPreset(value: Int, profile: Int = this.profile) {
dlog(TAG, "setIeqPreset: $value")
checkEffect()
dolbyEffect.setDapParameter(DsParam.IEQ_PRESET, value, profile)
}
companion object {
private const val TAG = "DolbyController"
private const val EFFECT_PRIORITY = 100
@Volatile
private var instance: DolbyController? = null
fun getInstance(context: Context) =
instance ?: synchronized(this) {
instance ?: DolbyController(context).also { instance = it }
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceManager
class DolbyPreferenceStore(
private val context: Context
) : PreferenceDataStore() {
private val defaultSharedPrefs by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
private lateinit var profileSharedPrefs: SharedPreferences
var profile = 0
set(value) {
field = value
profileSharedPrefs = context.getSharedPreferences(
"profile_$value",
Context.MODE_PRIVATE
)
}
private fun getSharedPreferences(key: String) =
if (DolbyConstants.PROFILE_SPECIFIC_PREFS.contains(key)) {
profileSharedPrefs
} else {
defaultSharedPrefs
}
override fun putBoolean(key: String, value: Boolean) =
getSharedPreferences(key).edit()
.putBoolean(key, value)
.apply()
override fun getBoolean(key: String, defValue: Boolean) =
getSharedPreferences(key).getBoolean(key, defValue)
override fun putInt(key: String, value: Int) =
getSharedPreferences(key).edit()
.putInt(key, value)
.apply()
override fun getInt(key: String, defValue: Int) =
getSharedPreferences(key).getInt(key, defValue)
override fun putString(key: String, value: String?) =
getSharedPreferences(key).edit()
.putString(key, value)
.apply()
override fun getString(key: String, defValue: String?) =
getSharedPreferences(key).getString(key, defValue)
}

View File

@@ -0,0 +1,266 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.media.AudioAttributes
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Bundle
import android.os.Handler
import android.widget.CompoundButton
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceChangeListener
import androidx.preference.PreferenceFragment
import androidx.preference.SwitchPreferenceCompat
import co.aospa.dolby.DolbyConstants.Companion.dlog
import co.aospa.dolby.R
import com.android.settingslib.widget.MainSwitchPreference
class DolbySettingsFragment : PreferenceFragment(),
OnPreferenceChangeListener, CompoundButton.OnCheckedChangeListener {
private val switchBar by lazy {
findPreference<MainSwitchPreference>(DolbyConstants.PREF_ENABLE)!!
}
private val profilePref by lazy {
findPreference<ListPreference>(DolbyConstants.PREF_PROFILE)!!
}
private val presetPref by lazy {
findPreference<Preference>(DolbyConstants.PREF_PRESET)!!
}
private val stereoPref by lazy {
findPreference<ListPreference>(DolbyConstants.PREF_STEREO)!!
}
private val dialoguePref by lazy {
findPreference<ListPreference>(DolbyConstants.PREF_DIALOGUE)!!
}
private val bassPref by lazy {
findPreference<SwitchPreferenceCompat>(DolbyConstants.PREF_BASS)!!
}
private val hpVirtPref by lazy {
findPreference<SwitchPreferenceCompat>(DolbyConstants.PREF_HP_VIRTUALIZER)!!
}
private val spkVirtPref by lazy {
findPreference<SwitchPreferenceCompat>(DolbyConstants.PREF_SPK_VIRTUALIZER)!!
}
private val volumePref by lazy {
findPreference<SwitchPreferenceCompat>(DolbyConstants.PREF_VOLUME)!!
}
private val resetPref by lazy {
findPreference<Preference>(DolbyConstants.PREF_RESET)!!
}
private val dolbyController by lazy { DolbyController.getInstance(context) }
private val audioManager by lazy { context.getSystemService(AudioManager::class.java) }
private val handler = Handler()
private var isOnSpeaker = true
set(value) {
if (field == value) return
field = value
dlog(TAG, "setIsOnSpeaker($value)")
updateProfileSpecificPrefs()
}
private val audioDeviceCallback = object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
dlog(TAG, "onAudioDevicesAdded")
updateSpeakerState()
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
dlog(TAG, "onAudioDevicesRemoved")
updateSpeakerState()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
dlog(TAG, "onCreatePreferences")
addPreferencesFromResource(R.xml.dolby_settings)
val profile = dolbyController.profile
preferenceManager.preferenceDataStore = DolbyPreferenceStore(context).also {
it.profile = profile
}
val dsOn = dolbyController.dsOn
switchBar.addOnSwitchChangeListener(this)
switchBar.setChecked(dsOn)
profilePref.onPreferenceChangeListener = this
profilePref.setEnabled(dsOn)
profilePref.apply {
if (entryValues.contains(profile.toString())) {
summary = "%s"
value = profile.toString()
} else {
summary = context.getString(R.string.dolby_unknown)
}
}
hpVirtPref.onPreferenceChangeListener = this
spkVirtPref.onPreferenceChangeListener = this
stereoPref.onPreferenceChangeListener = this
dialoguePref.onPreferenceChangeListener = this
bassPref.onPreferenceChangeListener = this
volumePref.onPreferenceChangeListener = this
resetPref.setOnPreferenceClickListener {
dolbyController.resetProfileSpecificSettings()
updateProfileSpecificPrefs()
Toast.makeText(
context,
context.getString(R.string.dolby_reset_profile_toast, profilePref.summary),
Toast.LENGTH_SHORT
).show()
true
}
audioManager!!.registerAudioDeviceCallback(audioDeviceCallback, handler)
updateSpeakerState()
updateProfileSpecificPrefs()
}
override fun onDestroyView() {
dlog(TAG, "onDestroyView")
audioManager!!.unregisterAudioDeviceCallback(audioDeviceCallback)
super.onDestroyView()
}
override fun onResume() {
super.onResume()
updateProfileSpecificPrefs()
}
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
dlog(TAG, "onPreferenceChange: key=${preference.key} value=$newValue")
when (preference.key) {
DolbyConstants.PREF_PROFILE -> {
val profile = newValue.toString().toInt()
dolbyController.profile = profile
(preferenceManager.preferenceDataStore as DolbyPreferenceStore).profile = profile
updateProfileSpecificPrefs()
}
DolbyConstants.PREF_SPK_VIRTUALIZER -> {
dolbyController.setSpeakerVirtEnabled(newValue as Boolean)
}
DolbyConstants.PREF_HP_VIRTUALIZER -> {
dolbyController.setHeadphoneVirtEnabled(newValue as Boolean)
}
DolbyConstants.PREF_STEREO -> {
dolbyController.setStereoWideningAmount(newValue.toString().toInt())
}
DolbyConstants.PREF_DIALOGUE -> {
dolbyController.setDialogueEnhancerAmount(newValue.toString().toInt())
}
DolbyConstants.PREF_BASS -> {
dolbyController.setBassEnhancerEnabled(newValue as Boolean)
}
DolbyConstants.PREF_VOLUME -> {
dolbyController.setVolumeLevelerEnabled(newValue as Boolean)
}
else -> return false
}
return true
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
dlog(TAG, "onCheckedChanged($isChecked)")
dolbyController.dsOn = isChecked
profilePref.setEnabled(isChecked)
updateProfileSpecificPrefs()
}
private fun updateSpeakerState() {
val device = audioManager!!.getDevicesForAttributes(ATTRIBUTES_MEDIA)[0]
isOnSpeaker = (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
}
private fun updateProfileSpecificPrefs() {
val unknownRes = context.getString(R.string.dolby_unknown)
val headphoneRes = context.getString(R.string.dolby_connect_headphones)
val dsOn = dolbyController.dsOn
val currentProfile = dolbyController.profile
dlog(
TAG, "updateProfileSpecificPrefs: dsOn=$dsOn currentProfile=$currentProfile"
+ " isOnSpeaker=$isOnSpeaker"
)
val enable = dsOn && (currentProfile != -1)
presetPref.setEnabled(enable)
spkVirtPref.setEnabled(enable)
dialoguePref.setEnabled(enable)
volumePref.setEnabled(enable)
resetPref.setEnabled(enable)
hpVirtPref.setEnabled(enable && !isOnSpeaker)
stereoPref.setEnabled(enable && !isOnSpeaker)
bassPref.setEnabled(enable)
if (!enable) return
presetPref.summary = dolbyController.getPresetName()
val deValue = dolbyController.getDialogueEnhancerAmount(currentProfile).toString()
dialoguePref.apply {
if (entryValues.contains(deValue)) {
summary = "%s"
value = deValue
} else {
summary = unknownRes
}
}
spkVirtPref.setChecked(dolbyController.getSpeakerVirtEnabled(currentProfile))
volumePref.setChecked(dolbyController.getVolumeLevelerEnabled(currentProfile))
// below prefs are not enabled on loudspeaker
if (isOnSpeaker) {
stereoPref.summary = headphoneRes
bassPref.summary = headphoneRes
hpVirtPref.summary = headphoneRes
return
}
val swValue = dolbyController.getStereoWideningAmount(currentProfile).toString()
stereoPref.apply {
if (entryValues.contains(swValue)) {
summary = "%s"
value = swValue
} else {
summary = unknownRes
}
}
bassPref.apply {
setChecked(dolbyController.getBassEnhancerEnabled(currentProfile))
summary = null
}
hpVirtPref.apply {
setChecked(dolbyController.getHeadphoneVirtEnabled(currentProfile))
summary = null
}
}
companion object {
private const val TAG = "DolbySettingsFragment"
private val ATTRIBUTES_MEDIA = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
private const val TAG = "DolbyTileService"
class DolbyTileService : TileService() {
private val dolbyController by lazy { DolbyController.getInstance(applicationContext) }
override fun onStartListening() {
qsTile.apply {
state = if (dolbyController.dsOn) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
subtitle = dolbyController.getProfileName() ?: getString(R.string.dolby_unknown)
updateTile()
}
super.onStartListening()
}
override fun onClick() {
val isDsOn = dolbyController.dsOn
dolbyController.dsOn = !isDsOn
qsTile.apply {
state = if (isDsOn) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE
updateTile()
}
super.onClick()
}
}

View File

@@ -1,77 +1,70 @@
/*
* 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)
}
}
}
/*
* Copyright (C) 2019 The Android Open Source Project
* (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import co.aospa.dolby.R
import com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY
private const val KEY_DOLBY = "dolby"
/** Provide preference summary for injected items. */
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 dolbyController = DolbyController.getInstance(context!!)
if (!dolbyController.dsOn) {
return context!!.getString(R.string.dolby_off)
}
return dolbyController.getProfileName()?.let {
context!!.getString(R.string.dolby_on_with_profile, it)
} ?: context!!.getString(R.string.dolby_on)
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.compose.rememberNavController
import co.aospa.dolby.R
import co.aospa.dolby.geq.ui.EqualizerScreen
import co.aospa.dolby.geq.ui.EqualizerViewModel
import com.android.settingslib.spa.framework.compose.localNavController
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.scaffold.SettingsScaffold
class EqualizerActivity : ComponentActivity() {
private val viewModel: EqualizerViewModel by viewModels { EqualizerViewModel.Factory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SettingsTheme {
MainContent()
}
}
}
@Composable
private fun MainContent() {
val navController = rememberNavController()
CompositionLocalProvider(navController.localNavController()) {
SettingsScaffold(
title = stringResource(id = R.string.dolby_preset)
) { paddingValues ->
EqualizerScreen(
viewModel = viewModel,
modifier = Modifier.padding(paddingValues)
)
}
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.data
data class BandGain(
val band: Int,
var gain: Int = 0
)

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.data
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import co.aospa.dolby.DolbyConstants.Companion.PREF_PRESET
import co.aospa.dolby.DolbyConstants.Companion.dlog
import co.aospa.dolby.DolbyController
import co.aospa.dolby.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.withContext
class EqualizerRepository(
private val context: Context
) {
private val dolbyController by lazy { DolbyController.getInstance(context) }
// Preset is saved as a string of comma separated gains in SharedPreferences
// and is unique to each profile ID
private val profile = dolbyController.profile
private val profileSharedPrefs by lazy {
context.getSharedPreferences(
"profile_$profile",
Context.MODE_PRIVATE
)
}
private val presetsSharedPrefs by lazy {
context.getSharedPreferences(
"presets",
Context.MODE_PRIVATE
)
}
val builtInPresets: List<Preset> by lazy {
val names = context.resources.getStringArray(
R.array.dolby_preset_entries
)
val presets = context.resources.getStringArray(
R.array.dolby_preset_values
)
List(names.size) { index ->
Preset(
name = names[index],
bandGains = deserializeGains(presets[index]),
)
}
}
val defaultPreset by lazy { builtInPresets[0] } // Flat
// User defined presets are stored in a SharedPreferences as
// key - preset name
// value - comma separated string of gains
val userPresets: Flow<List<Preset>> = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
dlog(TAG, "presetsSharedPrefs changed")
trySend(
presetsSharedPrefs.all.map { (key, value) ->
Preset(
name = key,
bandGains = deserializeGains(value.toString()),
isUserDefined = true
)
}
)
}
presetsSharedPrefs.registerOnSharedPreferenceChangeListener(listener)
dlog(TAG, "presetsSharedPrefs registered listener")
// trigger an initial emission
listener.onSharedPreferenceChanged(presetsSharedPrefs, null)
awaitClose {
presetsSharedPrefs.unregisterOnSharedPreferenceChangeListener(listener)
dlog(TAG, "presetsSharedPrefs unregistered listener")
}
}
suspend fun getBandGains(): List<BandGain> = withContext(Dispatchers.IO) {
val gains = profileSharedPrefs.getString(PREF_PRESET, dolbyController.getPreset())
return@withContext if (gains.isNullOrEmpty()) {
defaultPreset.bandGains
} else {
deserializeGains(gains)
}.also {
dlog(TAG, "getBandGains: $it")
}
}
suspend fun setBandGains(bandGains: List<BandGain>) = withContext(Dispatchers.IO) {
dlog(TAG, "setBandGains($bandGains)")
val gains = serializeGains(bandGains)
dolbyController.setPreset(gains)
profileSharedPrefs.edit()
.putString(PREF_PRESET, gains)
.apply()
}
suspend fun addPreset(preset: Preset) = withContext(Dispatchers.IO) {
dlog(TAG, "addPreset($preset)")
presetsSharedPrefs.edit()
.putString(preset.name, serializeGains(preset.bandGains))
.apply()
}
suspend fun removePreset(preset: Preset) = withContext(Dispatchers.IO) {
dlog(TAG, "removePreset($preset)")
presetsSharedPrefs.edit()
.remove(preset.name)
.apply()
}
private companion object {
const val TAG = "EqRepository"
val tenBandFreqs = intArrayOf(
32,
64,
125,
250,
500,
1000,
2000,
4000,
8000,
16000
)
fun deserializeGains(bandGains: String): List<BandGain> {
val gains: List<Int> =
bandGains.split(",").runCatching {
require(size == 20) {
"Preset must have 20 elements, has only $size!"
}
map { it.toInt() }
.twentyToTenBandGains()
}.onFailure { exception ->
Log.e(TAG, "Failed to parse preset", exception)
}.getOrDefault(
// fallback to flat
List<Int>(10) { 0 }
)
return List(10) { index ->
BandGain(
band = tenBandFreqs[index],
gain = gains[index]
)
}
}
fun serializeGains(bandGains: List<BandGain>): String {
return bandGains.map { it.gain }
.tenToTwentyBandGains()
.joinToString(",")
}
// we show only 10 bands in UI however backend requires 20 bands
fun List<Int>.tenToTwentyBandGains() =
List<Int>(20) { index ->
if (index % 2 == 1 && index < 19) {
// every odd element is the average of its surrounding elements
(this[(index - 1) / 2] + this[(index + 1) / 2]) / 2
} else {
this[index / 2]
}
}
fun List<Int>.twentyToTenBandGains() =
// skip every odd element
filterIndexed { index, _ -> index % 2 == 0 }
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.data
data class Preset(
var name: String,
val bandGains: List<BandGain>,
var isUserDefined: Boolean = false,
var isMutated: Boolean = false
)

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import co.aospa.dolby.geq.data.BandGain
@Composable
fun BandGainSlider(
bandGain: BandGain,
onValueChangeFinished: (Int) -> Unit
) {
// Gain range is of -1->1 in UI, -100->100 in backend, but actually is -10->10 dB.
// Ensure we update the slider when gain is changed,
// for eg. when changing the preset
var sliderPosition by remember(bandGain.gain) {
mutableFloatStateOf(bandGain.gain / 100f)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
SliderText(
"%.1f".format(sliderPosition * 10f)
)
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
onValueChangeFinished = {
onValueChangeFinished((sliderPosition * 100f).toInt())
},
valueRange = -1f..1f,
modifier = Modifier
.graphicsLayer {
rotationZ = 270f
transformOrigin = TransformOrigin(0f, 0f)
}
.layout { measurable, constraints ->
val placeable = measurable.measure(
Constraints(
minWidth = constraints.minHeight,
maxWidth = constraints.maxHeight,
minHeight = constraints.minWidth,
maxHeight = constraints.maxHeight,
)
)
layout(placeable.height, placeable.width) {
placeable.place(-placeable.width, 0)
}
}
// horizontal and vertical dimensions are inverted due to rotation
.width(200.dp)
.height(40.dp)
.padding(8.dp)
)
SliderText(
with(bandGain.band) {
if (this >= 1000) {
"${this / 1000}k"
} else {
"$this"
}
}
)
}
}
@Composable
fun SliderText(
text: String,
modifier: Modifier = Modifier
) {
Text(
text = text,
modifier = modifier,
fontSize = 12.sp
)
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import co.aospa.dolby.R
@Composable
fun BandGainSliderLabels() {
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier.padding(end = 8.dp)
) {
LabelText(
stringResource(id = R.string.dolby_geq_slider_label_gain)
)
Column(
modifier = Modifier.height(200.dp),
horizontalAlignment = Alignment.End
) {
LabelText(
"+10 dB",
modifier = Modifier.padding(
top = 10.dp
)
)
Spacer(
modifier = Modifier.weight(1f)
)
LabelText("0 dB")
Spacer(
modifier = Modifier.weight(1f)
)
LabelText(
"-10 dB",
modifier = Modifier.padding(
bottom = 10.dp
)
)
}
LabelText("Hz")
}
}
@Composable
fun LabelText(
text: String,
modifier: Modifier = Modifier
) {
Text(
text = text,
modifier = modifier,
color = MaterialTheme.colorScheme.secondary,
fontSize = 12.sp
)
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
@Composable
fun ConfirmationDialog(
text: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
var showDialog by remember { mutableStateOf(true) }
if (!showDialog) {
onDismiss()
return
}
AlertDialog(
onDismissRequest = { showDialog = false },
confirmButton = {
TextButton(
onClick = {
showDialog = false
onConfirm()
}
) {
Text(
stringResource(id = android.R.string.ok)
)
}
},
dismissButton = {
TextButton(
onClick = { showDialog = false }
) {
Text(
stringResource(id = android.R.string.cancel)
)
}
},
text = {
Text(text)
}
)
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun EqualizerBands(viewModel: EqualizerViewModel) {
val preset by viewModel.preset.collectAsState()
val bandGains = preset.bandGains
LazyRow(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
item {
BandGainSliderLabels()
}
items(bandGains.size) { index ->
BandGainSlider(
bandGains[index],
onValueChangeFinished = {
viewModel.setGain(index, it)
}
)
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsTheme
@Composable
fun EqualizerScreen(
viewModel: EqualizerViewModel,
modifier: Modifier = Modifier
) {
SettingsTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(SettingsDimension.itemPadding)
.then(modifier),
color = MaterialTheme.colorScheme.background
) {
Column(
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxHeight()
) {
PresetSelector(viewModel = viewModel)
EqualizerBands(viewModel = viewModel)
}
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.aospa.dolby.geq.data.EqualizerRepository
import co.aospa.dolby.geq.data.Preset
import co.aospa.dolby.DolbyConstants.Companion.dlog
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
const val TAG = "EqViewModel"
class EqualizerViewModel(
private val repository: EqualizerRepository
) : ViewModel() {
private val _presets = MutableStateFlow(repository.builtInPresets)
val presets = _presets.asStateFlow()
private val _preset = MutableStateFlow(repository.defaultPreset)
val preset = _preset.asStateFlow()
private var presetRestored = false
init {
// Update the list of presets: combined list of user defined presets if any,
// and then the built in presets.
repository.userPresets
.onEach { presets ->
dlog(TAG, "updated userPresets: $presets")
_presets.value = mutableListOf<Preset>().apply {
addAll(presets)
addAll(repository.builtInPresets)
}.toList()
// We can restore the active preset only after the presets list is populated,
// since we do not save the preset name but only its gains.
if (!presetRestored) {
val bandGains = repository.getBandGains()
_preset.value = _presets.value.find {
bandGains == it.bandGains
} ?: Preset(
name = "Custom",
bandGains = bandGains
)
dlog(TAG, "restored preset: ${_preset.value}")
presetRestored = true
}
}
.launchIn(viewModelScope)
// Update the preset in repository everytime we set it here
_preset
.drop(1) // skip the initial value
.onEach {
// wait till the active preset is restored
if (!presetRestored) {
return@onEach
}
dlog(TAG, "updated preset: $it")
repository.setBandGains(it.bandGains)
if (it.isUserDefined) {
repository.addPreset(it)
}
}
.launchIn(viewModelScope)
}
fun reset() {
dlog(TAG, "reset()")
if (_preset.value.isUserDefined) {
// Reset gains to 0
_preset.value = _preset.value.copy(
bandGains = repository.defaultPreset.bandGains
)
} else {
// Switch to flat preset
_preset.value = repository.defaultPreset
}
}
fun setPreset(preset: Preset) {
dlog(TAG, "setPreset($preset)")
_preset.value = preset
}
fun setGain(index: Int, gain: Int) {
dlog(TAG, "setGain($index, $gain)")
_preset.value = _preset.value.run {
copy(
name = if (!isUserDefined) "Custom" else name,
bandGains = bandGains
.toMutableList()
// create a new object to ensure the flow emits an update.
.apply { this[index] = this[index].copy(gain = gain) }
.toList(),
isMutated = true
)
}
}
// Returns string containing the error message if it failed, otherwise null
private fun validatePresetName(name: String): PresetNameValidationError? {
// Ensure we don't have another preset with the same name
return if (
_presets.value
.any { it.name.equals(name.trim(), ignoreCase = true) }
) {
PresetNameValidationError.NAME_EXISTS
} else if (name.length > 50) {
PresetNameValidationError.NAME_TOO_LONG
} else null
}
fun createNewPreset(name: String): PresetNameValidationError? {
dlog(TAG, "createNewPreset($name)")
validatePresetName(name)?.let {
dlog(TAG, "createNewPreset failed: $it")
return it
}
_preset.value = _preset.value.copy(
name = name.trim(),
isUserDefined = true,
isMutated = false
)
return null
}
fun renamePreset(preset: Preset, name: String): PresetNameValidationError? {
dlog(TAG, "renamePreset($preset, $name)")
// create a preset with the new name and same gains
createNewPreset(name = name)?.let {
dlog(TAG, "renamePreset failed")
return it
}
// and delete the old one.
deletePreset(preset, shouldReset = false)
return null
}
fun deletePreset(preset: Preset, shouldReset: Boolean = true) {
dlog(TAG, "deletePreset($preset)")
viewModelScope.launch {
repository.removePreset(preset)
}
if (shouldReset) {
_preset.value = repository.defaultPreset
}
}
companion object {
val Factory = viewModelFactory {
initializer {
EqualizerViewModel(
repository = EqualizerRepository(
this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!!
)
)
}
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import co.aospa.dolby.R
@Composable
fun PresetNameDialog(
title: String,
presetName: String = "",
onPresetNameSet: (String) -> PresetNameValidationError?,
onDismissDialog: () -> Unit
) {
var showDialog by remember { mutableStateOf(true) }
if (!showDialog) {
onDismissDialog()
return
}
var text by remember { mutableStateOf(presetName) }
var error by remember { mutableStateOf<PresetNameValidationError?>(null) }
AlertDialog(
onDismissRequest = { showDialog = false },
confirmButton = {
TextButton(
onClick = {
onPresetNameSet(text)?.let {
// validation failed
error = it
return@TextButton
}
// succeeded
showDialog = false
error = null
}
) {
Text(
stringResource(id = android.R.string.ok)
)
}
},
dismissButton = {
TextButton(
onClick = { showDialog = false }
) {
Text(
stringResource(id = android.R.string.cancel)
)
}
},
title = { Text(title) },
text = {
Column {
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = {
Text(
stringResource(id = R.string.dolby_geq_preset_name)
)
},
isError = error != null,
singleLine = true
)
error?.let {
Text(
text = it.toErrorMessage(),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
)
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import co.aospa.dolby.R
enum class PresetNameValidationError {
NAME_EXISTS,
NAME_TOO_LONG;
@Composable
fun toErrorMessage() =
stringResource(
id = when (this) {
NAME_EXISTS -> R.string.dolby_geq_preset_name_exists
NAME_TOO_LONG -> R.string.dolby_geq_preset_name_too_long
}
)
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import co.aospa.dolby.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PresetSelector(viewModel: EqualizerViewModel) {
val presets by viewModel.presets.collectAsState()
val currentPreset by viewModel.preset.collectAsState()
var expanded by remember { mutableStateOf(false) }
var showNewPresetDialog by remember { mutableStateOf(false) }
var showRenamePresetDialog by remember { mutableStateOf(false) }
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
var showResetConfirmDialog by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
modifier = Modifier
.padding(end = 8.dp)
.weight(1f)
) {
TextField(
value = currentPreset.name,
onValueChange = { },
readOnly = true,
label = {
Text(
stringResource(id = R.string.dolby_geq_preset)
)
},
singleLine = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
modifier = Modifier.menuAnchor()
// prevent keyboard from popping up
.focusProperties { canFocus = false }
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
presets.forEach { preset ->
DropdownMenuItem(
text = { Text(text = preset.name) },
onClick = {
viewModel.setPreset(preset)
expanded = false
}
)
}
}
}
TooltipIconButton(
icon = ImageVector.vectorResource(
id = R.drawable.save_as_24px
),
text = stringResource(id = R.string.dolby_geq_new_preset),
onClick = { showNewPresetDialog = true }
)
if (currentPreset.isUserDefined) {
TooltipIconButton(
icon = Icons.Default.Edit,
text = stringResource(id = R.string.dolby_geq_rename_preset),
onClick = { showRenamePresetDialog = true }
)
TooltipIconButton(
icon = Icons.Default.Delete,
text = stringResource(id = R.string.dolby_geq_delete_preset),
onClick = { showDeleteConfirmDialog = true }
)
}
TooltipIconButton(
icon = ImageVector.vectorResource(
id = R.drawable.reset_settings_24px
),
text = stringResource(id = R.string.dolby_geq_reset_gains),
onClick = {
if (currentPreset.isUserDefined) {
showResetConfirmDialog = true
} else {
viewModel.reset()
}
}
)
}
// Dialogs
if (showNewPresetDialog) {
PresetNameDialog(
title = stringResource(id = R.string.dolby_geq_new_preset),
onPresetNameSet = {
return@PresetNameDialog viewModel.createNewPreset(name = it)
},
onDismissDialog = { showNewPresetDialog = false }
)
}
if (showRenamePresetDialog) {
PresetNameDialog(
title = stringResource(id = R.string.dolby_geq_rename_preset),
presetName = currentPreset.name,
onPresetNameSet = {
return@PresetNameDialog viewModel.renamePreset(
preset = currentPreset,
name = it
)
},
onDismissDialog = { showRenamePresetDialog = false }
)
}
if (showDeleteConfirmDialog) {
ConfirmationDialog(
text = stringResource(id = R.string.dolby_geq_delete_preset_prompt),
onConfirm = { viewModel.deletePreset(currentPreset) },
onDismiss = { showDeleteConfirmDialog = false }
)
}
if (showResetConfirmDialog) {
ConfirmationDialog(
text = stringResource(id = R.string.dolby_geq_reset_gains_prompt),
onConfirm = { viewModel.reset() },
onDismiss = { showResetConfirmDialog = false }
)
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.geq.ui
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipIconButton(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
Text(text)
},
state = rememberTooltipState()
) {
IconButton(
onClick = onClick
) {
Icon(
imageVector = icon,
contentDescription = text,
modifier = Modifier.size(24.dp)
)
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.preference
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.preference.ListPreference
import androidx.preference.PreferenceViewHolder
import co.aospa.dolby.R
// Preference with icon on the right side
class DolbyIeqPreference(
context: Context,
attrs: AttributeSet?,
) : ListPreference(context, attrs) {
init {
widgetLayoutResource = R.layout.ieq_icon_layout
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val iconView = holder.findViewById(R.id.ieq_icon)!! as ImageView
val icon = AppCompatResources.getDrawable(context, getIeqIconResId())
iconView.setImageDrawable(icon)
}
private fun getIeqIconResId(): Int {
val ieqValue = value?.toIntOrNull() ?: 0
return when (ieqValue) {
0 -> R.drawable.ic_ieq_off
1 -> R.drawable.ic_ieq_balanced
2 -> R.drawable.ic_ieq_warm
3 -> R.drawable.ic_ieq_detailed
else -> 0 // should never hit this!
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2024 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.preference
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceManager
import co.aospa.dolby.DolbyConstants
class DolbyPreferenceStore(
private val context: Context
) : PreferenceDataStore() {
private val defaultSharedPrefs by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
private lateinit var profileSharedPrefs: SharedPreferences
var profile = 0
set(value) {
field = value
profileSharedPrefs = context.getSharedPreferences(
"profile_$value",
Context.MODE_PRIVATE
)
}
private fun getSharedPreferences(key: String) =
if (DolbyConstants.PROFILE_SPECIFIC_PREFS.contains(key)) {
profileSharedPrefs
} else {
defaultSharedPrefs
}
override fun putBoolean(key: String, value: Boolean) =
getSharedPreferences(key).edit()
.putBoolean(key, value)
.apply()
override fun getBoolean(key: String, defValue: Boolean) =
getSharedPreferences(key).getBoolean(key, defValue)
override fun putInt(key: String, value: Int) =
getSharedPreferences(key).edit()
.putInt(key, value)
.apply()
override fun getInt(key: String, defValue: Int) =
getSharedPreferences(key).getInt(key, defValue)
override fun putString(key: String, value: String?) =
getSharedPreferences(key).edit()
.putString(key, value)
.apply()
override fun getString(key: String, defValue: String?) =
getSharedPreferences(key).getString(key, defValue)
}

View File

@@ -0,0 +1,306 @@
/*
* Copyright (C) 2023-24 Paranoid Android
*
* SPDX-License-Identifier: Apache-2.0
*/
package co.aospa.dolby.preference
import android.media.AudioAttributes
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Bundle
import android.os.Handler
import android.widget.CompoundButton
import android.widget.CompoundButton.OnCheckedChangeListener
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceChangeListener
import androidx.preference.PreferenceFragment
import androidx.preference.SwitchPreferenceCompat
import co.aospa.dolby.DolbyConstants
import co.aospa.dolby.DolbyConstants.Companion.PREF_BASS
import co.aospa.dolby.DolbyConstants.Companion.PREF_DIALOGUE
import co.aospa.dolby.DolbyConstants.Companion.PREF_ENABLE
import co.aospa.dolby.DolbyConstants.Companion.PREF_HP_VIRTUALIZER
import co.aospa.dolby.DolbyConstants.Companion.PREF_IEQ
import co.aospa.dolby.DolbyConstants.Companion.PREF_PRESET
import co.aospa.dolby.DolbyConstants.Companion.PREF_PROFILE
import co.aospa.dolby.DolbyConstants.Companion.PREF_RESET
import co.aospa.dolby.DolbyConstants.Companion.PREF_SPK_VIRTUALIZER
import co.aospa.dolby.DolbyConstants.Companion.PREF_STEREO
import co.aospa.dolby.DolbyConstants.Companion.PREF_VOLUME
import co.aospa.dolby.DolbyConstants.Companion.dlog
import co.aospa.dolby.DolbyController
import co.aospa.dolby.R
import com.android.settingslib.widget.MainSwitchPreference
class DolbySettingsFragment : PreferenceFragment(),
OnPreferenceChangeListener, CompoundButton.OnCheckedChangeListener {
private val switchBar by lazy {
findPreference<MainSwitchPreference>(PREF_ENABLE)!!
}
private val profilePref by lazy {
findPreference<ListPreference>(PREF_PROFILE)!!
}
private val presetPref by lazy {
findPreference<Preference>(PREF_PRESET)!!
}
private val ieqPref by lazy {
findPreference<DolbyIeqPreference>(PREF_IEQ)!!
}
private val stereoPref by lazy {
findPreference<ListPreference>(PREF_STEREO)!!
}
private val dialoguePref by lazy {
findPreference<ListPreference>(PREF_DIALOGUE)!!
}
private val bassPref by lazy {
findPreference<SwitchPreferenceCompat>(PREF_BASS)!!
}
private val hpVirtPref by lazy {
findPreference<SwitchPreferenceCompat>(PREF_HP_VIRTUALIZER)!!
}
private val spkVirtPref by lazy {
findPreference<SwitchPreferenceCompat>(PREF_SPK_VIRTUALIZER)!!
}
private val volumePref by lazy {
findPreference<SwitchPreferenceCompat>(PREF_VOLUME)!!
}
private val resetPref by lazy {
findPreference<Preference>(PREF_RESET)!!
}
private val dolbyController by lazy { DolbyController.getInstance(context) }
private val audioManager by lazy { context.getSystemService(AudioManager::class.java) }
private val handler = Handler()
private var isOnSpeaker = true
set(value) {
if (field == value) return
field = value
dlog(TAG, "setIsOnSpeaker($value)")
updateProfileSpecificPrefs()
}
private val audioDeviceCallback = object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
dlog(TAG, "onAudioDevicesAdded")
updateSpeakerState()
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
dlog(TAG, "onAudioDevicesRemoved")
updateSpeakerState()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
dlog(TAG, "onCreatePreferences")
addPreferencesFromResource(R.xml.dolby_settings)
val profile = dolbyController.profile
preferenceManager.preferenceDataStore = DolbyPreferenceStore(context).also {
it.profile = profile
}
val dsOn = dolbyController.dsOn
switchBar.addOnSwitchChangeListener(this)
switchBar.setChecked(dsOn)
profilePref.onPreferenceChangeListener = this
updateProfileIcon(profile)
profilePref.setEnabled(dsOn)
profilePref.apply {
if (entryValues.contains(profile.toString())) {
summary = "%s"
value = profile.toString()
} else {
summary = context.getString(R.string.dolby_unknown)
}
}
hpVirtPref.onPreferenceChangeListener = this
spkVirtPref.onPreferenceChangeListener = this
stereoPref.onPreferenceChangeListener = this
dialoguePref.onPreferenceChangeListener = this
bassPref.onPreferenceChangeListener = this
volumePref.onPreferenceChangeListener = this
ieqPref.onPreferenceChangeListener = this
resetPref.setOnPreferenceClickListener {
dolbyController.resetProfileSpecificSettings()
updateProfileSpecificPrefs()
Toast.makeText(
context,
context.getString(R.string.dolby_reset_profile_toast, profilePref.summary),
Toast.LENGTH_SHORT
).show()
true
}
audioManager!!.registerAudioDeviceCallback(audioDeviceCallback, handler)
updateSpeakerState()
updateProfileSpecificPrefs()
}
override fun onDestroyView() {
dlog(TAG, "onDestroyView")
audioManager!!.unregisterAudioDeviceCallback(audioDeviceCallback)
super.onDestroyView()
}
override fun onResume() {
super.onResume()
updateProfileSpecificPrefs()
}
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
dlog(TAG, "onPreferenceChange: key=${preference.key} value=$newValue")
when (preference.key) {
PREF_PROFILE -> {
val profile = newValue.toString().toInt()
dolbyController.profile = profile
(preferenceManager.preferenceDataStore as DolbyPreferenceStore).profile = profile
updateProfileIcon(profile)
updateProfileSpecificPrefs()
}
PREF_SPK_VIRTUALIZER -> {
dolbyController.setSpeakerVirtEnabled(newValue as Boolean)
}
PREF_HP_VIRTUALIZER -> {
dolbyController.setHeadphoneVirtEnabled(newValue as Boolean)
}
PREF_STEREO -> {
dolbyController.setStereoWideningAmount(newValue.toString().toInt())
}
PREF_DIALOGUE -> {
dolbyController.setDialogueEnhancerAmount(newValue.toString().toInt())
}
PREF_BASS -> {
dolbyController.setBassEnhancerEnabled(newValue as Boolean)
}
PREF_VOLUME -> {
dolbyController.setVolumeLevelerEnabled(newValue as Boolean)
}
PREF_IEQ -> {
dolbyController.setIeqPreset(newValue.toString().toInt())
}
else -> return false
}
return true
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
dlog(TAG, "onCheckedChanged($isChecked)")
dolbyController.dsOn = isChecked
profilePref.setEnabled(isChecked)
updateProfileSpecificPrefs()
}
private fun updateSpeakerState() {
val device = audioManager!!.getDevicesForAttributes(ATTRIBUTES_MEDIA)[0]
isOnSpeaker = (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
}
private fun updateProfileSpecificPrefs() {
val unknownRes = context.getString(R.string.dolby_unknown)
val headphoneRes = context.getString(R.string.dolby_connect_headphones)
val dsOn = dolbyController.dsOn
val currentProfile = dolbyController.profile
dlog(
TAG, "updateProfileSpecificPrefs: dsOn=$dsOn currentProfile=$currentProfile"
+ " isOnSpeaker=$isOnSpeaker"
)
val enable = dsOn && (currentProfile != -1)
presetPref.setEnabled(enable)
spkVirtPref.setEnabled(enable)
ieqPref.setEnabled(enable)
dialoguePref.setEnabled(enable)
volumePref.setEnabled(enable)
bassPref.setEnabled(enable)
resetPref.setEnabled(enable)
hpVirtPref.setEnabled(enable && !isOnSpeaker)
stereoPref.setEnabled(enable && !isOnSpeaker)
if (!enable) return
presetPref.summary = dolbyController.getPresetName()
val ieqValue = dolbyController.getIeqPreset(currentProfile)
ieqPref.apply {
if (entryValues.contains(ieqValue.toString())) {
summary = "%s"
value = ieqValue.toString()
} else {
summary = unknownRes
}
}
val deValue = dolbyController.getDialogueEnhancerAmount(currentProfile).toString()
dialoguePref.apply {
if (entryValues.contains(deValue)) {
summary = "%s"
value = deValue
} else {
summary = unknownRes
}
}
spkVirtPref.setChecked(dolbyController.getSpeakerVirtEnabled(currentProfile))
volumePref.setChecked(dolbyController.getVolumeLevelerEnabled(currentProfile))
bassPref.setChecked(dolbyController.getBassEnhancerEnabled(currentProfile))
// below prefs are not enabled on loudspeaker
if (isOnSpeaker) {
stereoPref.summary = headphoneRes
hpVirtPref.summary = headphoneRes
return
}
val swValue = dolbyController.getStereoWideningAmount(currentProfile).toString()
stereoPref.apply {
if (entryValues.contains(swValue)) {
summary = "%s"
value = swValue
} else {
summary = unknownRes
}
}
hpVirtPref.apply {
setChecked(dolbyController.getHeadphoneVirtEnabled(currentProfile))
summary = null
}
}
private fun updateProfileIcon(profile: Int) {
when (profile) {
0 -> profilePref.setIcon(R.drawable.ic_profile_dynamic)
1 -> profilePref.setIcon(R.drawable.ic_profile_movie)
2 -> profilePref.setIcon(R.drawable.ic_profile_music)
3 -> profilePref.setIcon(R.drawable.ic_profile_custom)
else -> profilePref.setIcon(R.drawable.ic_dolby)
}
}
companion object {
private const val TAG = "DolbySettingsFragment"
private val ATTRIBUTES_MEDIA = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
}
}

View File

@@ -1,9 +1,9 @@
AOSPA Dolby
Sony Dolby
==============
Getting Started
---------------
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) and make sure your device supports c2 codecs. :-
```bash
<Include href="media_codecs_dolby_audio.xml" />
@@ -42,20 +42,4 @@ 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)
https://github.com/Neoteric-OS/device_nothing_Pong/commit/cefa46567c04770e492351e51dd96cfae80e34fb

View File

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

View File

@@ -1,20 +0,0 @@
<?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>

View File

@@ -287,32 +287,6 @@
<band_geq frequency="19688" gain="0"/>
</graphic-equalizer-bands>
</data>
</preset>
<preset id="8" type="geq">
<data>
<graphic-equalizer-bands>
<band_geq frequency="47" gain="0"/>
<band_geq frequency="141" gain="0"/>
<band_geq frequency="234" gain="0"/>
<band_geq frequency="328" gain="0"/>
<band_geq frequency="469" gain="0"/>
<band_geq frequency="656" gain="0"/>
<band_geq frequency="844" gain="0"/>
<band_geq frequency="1031" gain="0"/>
<band_geq frequency="1313" gain="0"/>
<band_geq frequency="1688" gain="0"/>
<band_geq frequency="2250" gain="0"/>
<band_geq frequency="3000" gain="0"/>
<band_geq frequency="3750" gain="0"/>
<band_geq frequency="4688" gain="0"/>
<band_geq frequency="5813" gain="0"/>
<band_geq frequency="7125" gain="0"/>
<band_geq frequency="9000" gain="0"/>
<band_geq frequency="11250" gain="0"/>
<band_geq frequency="13875" gain="0"/>
<band_geq frequency="19688" gain="0"/>
</graphic-equalizer-bands>
</data>
</preset>
<profile id="0" name="Dynamic" group="default">
<data>
@@ -336,14 +310,14 @@
<peak-value value="256"/>
<surround-decoder-enable value="true"/>
<surround-boost value="0"/>
<volmax-boost value="98"/>
<volume-leveler-enable value="true"/>
<volume-leveler-amount value="1"/>
<volmax-boost value="50"/>
<volume-leveler-enable value="false"/>
<volume-leveler-amount value="0"/>
<volume-leveler-in-target value="-256"/>
<volume-leveler-out-target value="-256"/>
<graphic-equalizer-enable value="true"/>
<virtualizer-enable value="false"/>
<bass-enhancer-enable value="true"/>
<bass-enhancer-enable value="false"/>
<virtual-bass-process-enable value="true"/>
<hearing-protection-enable value="false"/>
<hearing-protection-rms-averaging-time-win-len value="6"/>
@@ -436,13 +410,13 @@
<surround-decoder-enable value="true"/>
<surround-boost value="48"/>
<volmax-boost value="42"/>
<volume-leveler-enable value="true"/>
<volume-leveler-enable value="false"/>
<volume-leveler-amount value="1"/>
<volume-leveler-in-target value="-256"/>
<volume-leveler-out-target value="-256"/>
<graphic-equalizer-enable value="true"/>
<virtualizer-enable value="true"/>
<bass-enhancer-enable value="true"/>
<bass-enhancer-enable value="false"/>
<virtual-bass-process-enable value="true"/>
<hearing-protection-enable value="false"/>
<hearing-protection-rms-averaging-time-win-len value="6"/>
@@ -534,8 +508,8 @@
<peak-value value="256"/>
<surround-decoder-enable value="true"/>
<surround-boost value="0"/>
<volmax-boost value="64"/>
<volume-leveler-enable value="true"/>
<volmax-boost value="50"/>
<volume-leveler-enable value="false"/>
<volume-leveler-amount value="0"/>
<volume-leveler-in-target value="-256"/>
<volume-leveler-out-target value="-256"/>
@@ -1047,108 +1021,9 @@
<headphone-virtualizer-steerer-source-distance value="4"/>
</endpoint_type>
<endpoint_type id="headphone">
<ieq-enable value="false"/>
<include preset="ieq_balanced"/>
<ieq-amount value="6"/>
<include preset="ieq_warm"/>
<calibration-boost value="0"/>
<dialog-enhancer-enable value="false"/>
<dialog-enhancer-amount value="6"/>
<dialog-enhancer-ducking value="0"/>
<mi-virtualizer-binaural-steering-enable value="false"/>
<peak-value value="256"/>
<surround-decoder-enable value="true"/>
<surround-boost value="0"/>
<volmax-boost value="32"/>
<volume-leveler-enable value="true"/>
<volume-leveler-amount value="0"/>
<volume-leveler-in-target value="-256"/>
<volume-leveler-out-target value="-256"/>
<graphic-equalizer-enable value="true"/>
<virtualizer-enable value="false"/>
<bass-enhancer-enable value="false"/>
<virtual-bass-process-enable value="false"/>
<hearing-protection-enable value="false"/>
<hearing-protection-rms-averaging-time-win-len value="6"/>
<hearing-protection-rms-attenuation-target value="-256"/>
<hearing-protection-attenuation-attack-time value="900"/>
<hearing-protection-attenuation-release-time value="1600"/>
<virtualizer-start-band value="0"/>
<headphone-virtualizer-steerer-source-distance value="4"/>
</endpoint_type>
<endpoint_type id="other">
<ieq-enable value="false"/>
<ieq-amount value="6"/>
<include preset="ieq_warm"/>
<calibration-boost value="0"/>
<dialog-enhancer-enable value="false"/>
<dialog-enhancer-amount value="6"/>
<dialog-enhancer-ducking value="0"/>
<mi-virtualizer-binaural-steering-enable value="false"/>
<peak-value value="256"/>
<surround-decoder-enable value="true"/>
<surround-boost value="0"/>
<volmax-boost value="32"/>
<volume-leveler-enable value="true"/>
<volume-leveler-amount value="0"/>
<volume-leveler-in-target value="-256"/>
<volume-leveler-out-target value="-256"/>
<graphic-equalizer-enable value="true"/>
<virtualizer-enable value="false"/>
<bass-enhancer-enable value="false"/>
<virtual-bass-process-enable value="false"/>
<hearing-protection-enable value="false"/>
<hearing-protection-rms-averaging-time-win-len value="6"/>
<hearing-protection-rms-attenuation-target value="-256"/>
<hearing-protection-attenuation-attack-time value="900"/>
<hearing-protection-attenuation-release-time value="1600"/>
<virtualizer-start-band value="0"/>
<headphone-virtualizer-steerer-source-distance value="4"/>
</endpoint_type>
</data>
</profile>
<profile id="8" name="Voice" group="default">
<data>
<mi-dv-leveler-steering-enable value="false"/>
<mi-ieq-steering-enable value="false"/>
<mi-surround-compressor-steering-enable value="false"/>
<mi-adaptive-virtualizer-steering-enable value="false"/>
<volume-modeler-enable value="false"/>
<drc-metadata/>
<reverb-suppression-enable value="false"/>
<reverb-suppression-amount value="9"/>
<endpoint_type id="speaker">
<ieq-enable value="true"/>
<ieq-amount value="11"/>
<include preset="ieq_detailed"/>
<calibration-boost value="0"/>
<dialog-enhancer-enable value="true"/>
<dialog-enhancer-amount value="9"/>
<dialog-enhancer-ducking value="0"/>
<mi-virtualizer-binaural-steering-enable value="false"/>
<peak-value value="240"/>
<surround-decoder-enable value="true"/>
<surround-boost value="0"/>
<volmax-boost value="32"/>
<volume-leveler-enable value="true"/>
<volume-leveler-amount value="0"/>
<volume-leveler-in-target value="-256"/>
<volume-leveler-out-target value="-256"/>
<graphic-equalizer-enable value="true"/>
<virtualizer-enable value="false"/>
<bass-enhancer-enable value="false"/>
<virtual-bass-process-enable value="true"/>
<hearing-protection-enable value="false"/>
<hearing-protection-rms-averaging-time-win-len value="6"/>
<hearing-protection-rms-attenuation-target value="-256"/>
<hearing-protection-attenuation-attack-time value="900"/>
<hearing-protection-attenuation-release-time value="1600"/>
<virtualizer-start-band value="0"/>
<headphone-virtualizer-steerer-source-distance value="4"/>
</endpoint_type>
<endpoint_type id="headphone">
<ieq-enable value="true"/>
<ieq-amount value="8"/>
<include preset="ieq_detailed"/>
<calibration-boost value="0"/>
<dialog-enhancer-enable value="true"/>
<dialog-enhancer-amount value="9"/>

View File

@@ -1,14 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
<!-- Copyright 2016 The Android Open Source Project
This program is protected under international and U.S. copyright laws as
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
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>
@@ -33,16 +49,7 @@
<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>

View File

@@ -0,0 +1,19 @@
<compatibility-matrix version="4.0" type="framework">
<hal format="hidl" optional="true">
<name>vendor.dolby.hardware.dms</name>
<version>2.0</version>
<interface>
<name>IDms</name>
<instance>default</instance>
</interface>
</hal>
<hal format="hidl" optional="true">
<name>android.hardware.media.c2</name>
<version>1.0</version>
<interface>
<name>IComponentStore</name>
<instance>dolby</instance>
<instance>default1</instance>
</interface>
</hal>
</compatibility-matrix>

View File

@@ -0,0 +1,11 @@
<manifest version="1.0" type="device">
<hal format="hidl">
<name>vendor.dolby.hardware.dms</name>
<transport>hwbinder</transport>
<version>2.0</version>
<interface>
<name>IDms</name>
<instance>default</instance>
</interface>
</hal>
</manifest>

View File

@@ -0,0 +1,19 @@
<!--
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.
-->
<manifest version="1.0" type="device">
<hal format="hidl">
<name>android.hardware.media.c2</name>
<transport>hwbinder</transport>
<fqname>@1.0::IComponentStore/default1</fqname>
</hal>
</manifest>

140
dolby.mk
View File

@@ -21,111 +21,63 @@ DOLBY_PATH := hardware/dolby
PRODUCT_SOONG_NAMESPACES += \
$(DOLBY_PATH)
# Enable codec support
AUDIO_FEATURE_ENABLED_DS2_DOLBY_DAP := true
# Build codec2 packages
PRODUCT_PACKAGES += \
libavservices_minijail.vendor \
libcodec2_hidl@1.2.vendor \
libcodec2_soft_common.vendor
# SEPolicy
BOARD_VENDOR_SEPOLICY_DIRS += $(DOLBY_PATH)/sepolicy/vendor
# HIDL
DEVICE_FRAMEWORK_COMPATIBILITY_MATRIX_FILE += $(DOLBY_PATH)/configs/vintf/dolby_framework_matrix.xml
DEVICE_MANIFEST_FILE += \
$(DOLBY_PATH)/configs/vintf/vendor.dolby.hardware.dms@2.0-service.xml \
$(DOLBY_PATH)/configs/vintf/vendor.dolby.media.c2@1.0-service.xml
# Configs
PRODUCT_COPY_FILES += \
$(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_PATH)/configs/dax/dax-default.xml:$(TARGET_COPY_OUT_VENDOR)/etc/dolby/dax-default.xml \
$(DOLBY_PATH)/configs/media/media_codecs_dolby_audio.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_codecs_dolby_audio.xml
# Dolby VNDK libs
PRODUCT_PACKAGES += \
libstagefright_foundation-v33
PRODUCT_PACKAGES += \
libshim_dolby
# Init
PRODUCT_PACKAGES += \
init.dolby.rc
# Overlays
PRODUCT_PACKAGES += \
DolbyFrameworksResCommon
# Dolby Spatial Audio
PRODUCT_COPY_FILES += \
$(DOLBY_PATH)/configs/android.hardware.sensor.dynamic.head_tracker.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.dynamic.head_tracker.xml \
prebuilts/vndk/v33/arm/arch-arm-armv7-a-neon/shared/vndk-core/libstagefright_foundation.so:$(TARGET_COPY_OUT_VENDOR)/lib/libstagefright_foundation-v33.so \
prebuilts/vndk/v33/arm64/arch-arm64-armv8-a/shared/vndk-core/libstagefright_foundation.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libstagefright_foundation-v33.so
# 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
# Dolby
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
ro.vendor.dolby.dax.version=DAX3_3.7.0.8_r1
# 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
# DolbyManager
PRODUCT_PACKAGES += \
RemovePackagesDolby
DolbyManager
# XiaomiDolby
PRODUCT_PACKAGES += \
XiaomiDolby
# Dolby Proprietary blobs
# Proprietary-files
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
$(DOLBY_PATH)/proprietary/vendor/bin/hw/vendor.dolby.hardware.dms@2.0-service:$(TARGET_COPY_OUT_VENDOR)/bin/hw/vendor.dolby.hardware.dms@2.0-service \
$(DOLBY_PATH)/proprietary/vendor/bin/hw/vendor.dolby.media.c2@1.0-service:$(TARGET_COPY_OUT_VENDOR)/bin/hw/vendor.dolby.media.c2@1.0-service \
$(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 \
$(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 \
$(DOLBY_PATH)/proprietary/vendor/lib/libdapparamstorage.so:$(TARGET_COPY_OUT_VENDOR)/lib/libdapparamstorage.so \
$(DOLBY_PATH)/proprietary/vendor/lib/libdlbpreg.so:$(TARGET_COPY_OUT_VENDOR)/lib/libdlbpreg.so \
$(DOLBY_PATH)/proprietary/vendor/lib/soundfx/libdlbvol.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libdlbvol.so \
$(DOLBY_PATH)/proprietary/vendor/lib/soundfx/libswdap.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libswdap.so \
$(DOLBY_PATH)/proprietary/vendor/lib/soundfx/libswgamedap.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libswgamedap.so \
$(DOLBY_PATH)/proprietary/vendor/lib/soundfx/libswvqe.so:$(TARGET_COPY_OUT_VENDOR)/lib/soundfx/libswvqe.so \
$(DOLBY_PATH)/proprietary/vendor/lib/vendor.dolby.hardware.dms@2.0.so:$(TARGET_COPY_OUT_VENDOR)/lib/vendor.dolby.hardware.dms@2.0.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libcodec2_soft_ac4dec.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libcodec2_soft_ac4dec.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libcodec2_soft_ddpdec.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libcodec2_soft_ddpdec.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libcodec2_soft_dolby.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libcodec2_soft_dolby.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libcodec2_store_dolby.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libcodec2_store_dolby.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libdapparamstorage.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdapparamstorage.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libdeccfg.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdeccfg.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libdlbdsservice.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdlbdsservice.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/libdlbpreg.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libdlbpreg.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/soundfx/libdlbvol.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libdlbvol.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/soundfx/libswdap.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libswdap.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/soundfx/libswgamedap.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libswgamedap.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/soundfx/libswvqe.so:$(TARGET_COPY_OUT_VENDOR)/lib64/soundfx/libswvqe.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/vendor.dolby.hardware.dms@2.0-impl.so:$(TARGET_COPY_OUT_VENDOR)/lib64/vendor.dolby.hardware.dms@2.0-impl.so \
$(DOLBY_PATH)/proprietary/vendor/lib64/vendor.dolby.hardware.dms@2.0.so:$(TARGET_COPY_OUT_VENDOR)/lib64/vendor.dolby.hardware.dms@2.0.so

View File

@@ -1,45 +0,0 @@
//
// 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,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,305 +0,0 @@
/*
* 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"
}
}

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