Compare commits
28 Commits
bka-aospa-
...
vic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0420f3f245 | ||
|
|
ea81a2acc2 | ||
|
|
fc013ffdef | ||
|
|
d69c31b038 | ||
|
|
f479eef03b | ||
|
|
e8ebd395d1 | ||
|
|
296d340b6d | ||
|
|
41a93dd519 | ||
|
|
750b00415a | ||
|
|
a5e3d9c63d | ||
|
|
f3fc9e46e2 | ||
|
|
939e58c22a | ||
|
|
4ec5eeda3c | ||
|
|
b2e635a522 | ||
|
|
42005cbb91 | ||
|
|
3b0d7fd56d | ||
|
|
01a21f5098 | ||
|
|
4dc3303a4c | ||
|
|
3fb780dd60 | ||
|
|
4be460186b | ||
|
|
d56e3b306b | ||
|
|
5c1afecdb5 | ||
|
|
8d0cda23b6 | ||
|
|
3a6b0ad303 | ||
|
|
deb7018300 | ||
|
|
c8c233e40d | ||
|
|
5bd62626da | ||
|
|
f28d0a3718 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.vscode/
|
||||
297
Android.bp
297
Android.bp
@@ -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
28
DolbyManager/Android.bp
Normal 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",
|
||||
],
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
8
DolbyManager/res/drawable/ic_dolby.xml
Normal file
8
DolbyManager/res/drawable/ic_dolby.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24" 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>
|
||||
@@ -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>
|
||||
24
DolbyManager/res/drawable/ic_ieq_balanced.xml
Normal file
24
DolbyManager/res/drawable/ic_ieq_balanced.xml
Normal 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>
|
||||
24
DolbyManager/res/drawable/ic_ieq_detailed.xml
Normal file
24
DolbyManager/res/drawable/ic_ieq_detailed.xml
Normal 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>
|
||||
13
DolbyManager/res/drawable/ic_ieq_off.xml
Normal file
13
DolbyManager/res/drawable/ic_ieq_off.xml
Normal 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>
|
||||
28
DolbyManager/res/drawable/ic_ieq_warm.xml
Normal file
28
DolbyManager/res/drawable/ic_ieq_warm.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
11
DolbyManager/res/drawable/ic_profile_custom.xml
Executable file
11
DolbyManager/res/drawable/ic_profile_custom.xml
Executable 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>
|
||||
11
DolbyManager/res/drawable/ic_profile_dynamic.xml
Executable file
11
DolbyManager/res/drawable/ic_profile_dynamic.xml
Executable 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>
|
||||
11
DolbyManager/res/drawable/ic_profile_movie.xml
Executable file
11
DolbyManager/res/drawable/ic_profile_movie.xml
Executable 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>
|
||||
11
DolbyManager/res/drawable/ic_profile_music.xml
Executable file
11
DolbyManager/res/drawable/ic_profile_music.xml
Executable 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>
|
||||
9
DolbyManager/res/drawable/reset_settings_24px.xml
Normal file
9
DolbyManager/res/drawable/reset_settings_24px.xml
Normal 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>
|
||||
9
DolbyManager/res/drawable/save_as_24px.xml
Normal file
9
DolbyManager/res/drawable/save_as_24px.xml
Normal 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>
|
||||
63
DolbyManager/res/layout-v34/settingslib_main_switch_bar.xml
Normal file
63
DolbyManager/res/layout-v34/settingslib_main_switch_bar.xml
Normal 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>
|
||||
41
DolbyManager/res/layout/ieq_icon_layout.xml
Normal file
41
DolbyManager/res/layout/ieq_icon_layout.xml
Normal 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>
|
||||
26
DolbyManager/res/layout/preference_widget_switch_compat.xml
Normal file
26
DolbyManager/res/layout/preference_widget_switch_compat.xml
Normal 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" />
|
||||
98
DolbyManager/res/values/arrays.xml
Normal file
98
DolbyManager/res/values/arrays.xml
Normal 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>
|
||||
70
DolbyManager/res/values/strings.xml
Normal file
70
DolbyManager/res/values/strings.xml
Normal 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>
|
||||
76
DolbyManager/res/xml/dolby_settings.xml
Normal file
76
DolbyManager/res/xml/dolby_settings.xml
Normal 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>
|
||||
27
DolbyManager/src/co/aospa/dolby/BootCompletedReceiver.kt
Normal file
27
DolbyManager/src/co/aospa/dolby/BootCompletedReceiver.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
23
DolbyManager/src/co/aospa/dolby/DolbyActivity.kt
Normal file
23
DolbyManager/src/co/aospa/dolby/DolbyActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
60
DolbyManager/src/co/aospa/dolby/DolbyConstants.kt
Normal file
60
DolbyManager/src/co/aospa/dolby/DolbyConstants.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
316
DolbyManager/src/co/aospa/dolby/DolbyController.kt
Normal file
316
DolbyManager/src/co/aospa/dolby/DolbyController.kt
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
63
DolbyManager/src/co/aospa/dolby/DolbyPreferenceStore.kt
Normal file
63
DolbyManager/src/co/aospa/dolby/DolbyPreferenceStore.kt
Normal 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)
|
||||
}
|
||||
266
DolbyManager/src/co/aospa/dolby/DolbySettingsFragment.kt
Normal file
266
DolbyManager/src/co/aospa/dolby/DolbySettingsFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
36
DolbyManager/src/co/aospa/dolby/DolbyTileService.kt
Normal file
36
DolbyManager/src/co/aospa/dolby/DolbyTileService.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
53
DolbyManager/src/co/aospa/dolby/geq/EqualizerActivity.kt
Normal file
53
DolbyManager/src/co/aospa/dolby/geq/EqualizerActivity.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
DolbyManager/src/co/aospa/dolby/geq/data/BandGain.kt
Normal file
12
DolbyManager/src/co/aospa/dolby/geq/data/BandGain.kt
Normal 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
|
||||
)
|
||||
183
DolbyManager/src/co/aospa/dolby/geq/data/EqualizerRepository.kt
Normal file
183
DolbyManager/src/co/aospa/dolby/geq/data/EqualizerRepository.kt
Normal 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 }
|
||||
}
|
||||
}
|
||||
14
DolbyManager/src/co/aospa/dolby/geq/data/Preset.kt
Normal file
14
DolbyManager/src/co/aospa/dolby/geq/data/Preset.kt
Normal 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
|
||||
)
|
||||
101
DolbyManager/src/co/aospa/dolby/geq/ui/BandGainSlider.kt
Normal file
101
DolbyManager/src/co/aospa/dolby/geq/ui/BandGainSlider.kt
Normal 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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
58
DolbyManager/src/co/aospa/dolby/geq/ui/ConfirmationDialog.kt
Normal file
58
DolbyManager/src/co/aospa/dolby/geq/ui/ConfirmationDialog.kt
Normal 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
39
DolbyManager/src/co/aospa/dolby/geq/ui/EqualizerBands.kt
Normal file
39
DolbyManager/src/co/aospa/dolby/geq/ui/EqualizerBands.kt
Normal 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
DolbyManager/src/co/aospa/dolby/geq/ui/EqualizerScreen.kt
Normal file
45
DolbyManager/src/co/aospa/dolby/geq/ui/EqualizerScreen.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
DolbyManager/src/co/aospa/dolby/geq/ui/EqualizerViewModel.kt
Normal file
175
DolbyManager/src/co/aospa/dolby/geq/ui/EqualizerViewModel.kt
Normal 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]!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
DolbyManager/src/co/aospa/dolby/geq/ui/PresetNameDialog.kt
Normal file
94
DolbyManager/src/co/aospa/dolby/geq/ui/PresetNameDialog.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
176
DolbyManager/src/co/aospa/dolby/geq/ui/PresetSelector.kt
Normal file
176
DolbyManager/src/co/aospa/dolby/geq/ui/PresetSelector.kt
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
46
DolbyManager/src/co/aospa/dolby/geq/ui/TooltipIconButton.kt
Normal file
46
DolbyManager/src/co/aospa/dolby/geq/ui/TooltipIconButton.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
22
README.mkdn
22
README.mkdn
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
@@ -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>
|
||||
19
configs/vintf/dolby_framework_matrix.xml
Normal file
19
configs/vintf/dolby_framework_matrix.xml
Normal 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>
|
||||
11
configs/vintf/vendor.dolby.hardware.dms@2.0-service.xml
Normal file
11
configs/vintf/vendor.dolby.hardware.dms@2.0-service.xml
Normal 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>
|
||||
19
configs/vintf/vendor.dolby.media.c2@1.0-service.xml
Normal file
19
configs/vintf/vendor.dolby.media.c2@1.0-service.xml
Normal 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
140
dolby.mk
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">@android:color/system_accent1_400</color>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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&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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user