mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-07-08 23:25:01 +02:00
android: 'testing' for Play relase
yeah... no big changes, unfortunately
This commit is contained in:
@@ -1,42 +1,74 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.aboutLibraries)
|
alias(libs.plugins.aboutLibraries)
|
||||||
|
// alias(libs.plugins.hilt)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val props = Properties().apply {
|
||||||
|
load(rootProject.file("local.properties").inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(props["RELEASE_STORE_FILE"] as String)
|
||||||
|
storePassword = props["RELEASE_STORE_PASSWORD"] as String
|
||||||
|
keyAlias = props["RELEASE_KEY_ALIAS"] as String
|
||||||
|
keyPassword = props["RELEASE_KEY_PASSWORD"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
namespace = "me.kavishdevar.librepods"
|
namespace = "me.kavishdevar.librepods"
|
||||||
compileSdk = 36
|
compileSdk = 37
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.kavishdevar.librepods"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
minSdk = 33
|
minSdk = 36
|
||||||
targetSdk = 36
|
targetSdk = 37
|
||||||
versionCode = 10
|
versionCode = 21
|
||||||
versionName = "0.2.0-alpha.2"
|
versionName = "0.2.0-beta.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments += "-DCMAKE_BUILD_TYPE=Release"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
create("playRelease") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
versionNameSuffix = "-play"
|
||||||
|
buildConfigField("Boolean", "PLAY_BUILD", "true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
jvmTarget = "1.8"
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_21)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
androidResources {
|
androidResources {
|
||||||
generateLocaleConfig = true
|
generateLocaleConfig = true
|
||||||
@@ -49,17 +81,41 @@ android {
|
|||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") {
|
||||||
res.srcDirs("src/main/res", "src/main/res-apple")
|
res.directories+="src/main/res-apple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ndkVersion = "30.0.14904198"
|
||||||
|
|
||||||
|
flavorDimensions += "env"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("normal") {
|
||||||
|
dimension = "env"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments += "-DIS_XPOSED=OFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
create("xposed") {
|
||||||
|
dimension = "env"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments += "-DIS_XPOSED=ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applicationIdSuffix = ".xposed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
|
||||||
implementation(libs.androidx.ui)
|
implementation(libs.androidx.ui)
|
||||||
implementation(libs.androidx.ui.graphics)
|
implementation(libs.androidx.ui.graphics)
|
||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
@@ -71,15 +127,17 @@ dependencies {
|
|||||||
implementation(libs.haze.materials)
|
implementation(libs.haze.materials)
|
||||||
implementation(libs.androidx.dynamicanimation)
|
implementation(libs.androidx.dynamicanimation)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
|
implementation(libs.androidx.compose.material.icons.core)
|
||||||
|
implementation(libs.billing)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
implementation(libs.androidx.compose.foundation.layout)
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
implementation(libs.aboutlibraries)
|
implementation(libs.aboutlibraries)
|
||||||
implementation(libs.aboutlibraries.compose.m3)
|
implementation(libs.aboutlibraries.compose.m3)
|
||||||
// compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
implementation(libs.backdrop)
|
||||||
// implementation(fileTree(mapOf("dir" to "lib", "include" to listOf("*.aar"))))
|
implementation(libs.hilt)
|
||||||
compileOnly(files("libs/libxposed-api-100.aar"))
|
// implementation(libs.hilt.compiler)
|
||||||
debugImplementation(files("libs/backdrop-debug.aar"))
|
add("xposedCompileOnly", files("libs/libxposed-api-100.aar"))
|
||||||
releaseImplementation(files("libs/backdrop-release.aar"))
|
add("playReleaseImplementation", libs.billing)
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Vendored
+3
@@ -19,3 +19,6 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-keep class androidx.compose.** { *; }
|
||||||
|
-dontwarn androidx.compose.**
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission
|
<!-- <uses-permission-->
|
||||||
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
<!-- android:name="android.permission.BLUETOOTH_PRIVILEGED"-->
|
||||||
tools:ignore="ProtectedPermissions" />
|
<!-- tools:ignore="ProtectedPermissions" />-->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.BLUETOOTH_SCAN"
|
android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
@@ -26,13 +26,14 @@
|
|||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<!-- <uses-permission android:name="android.permission.INTERNET" />-->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"-->
|
||||||
android:maxSdkVersion="30" />
|
<!-- android:maxSdkVersion="30" />-->
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
|
<!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"-->
|
||||||
android:maxSdkVersion="30" />
|
<!-- android:maxSdkVersion="30" />-->
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -45,8 +46,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.LibrePods"
|
android:theme="@style/Theme.LibrePods"
|
||||||
android:description="@string/app_description"
|
android:description="@string/app_description"
|
||||||
tools:ignore="UnusedAttribute"
|
tools:ignore="UnusedAttribute" >
|
||||||
tools:targetApi="31">
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widgets.NoiseControlWidget"
|
android:name=".widgets.NoiseControlWidget"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
@@ -114,17 +114,17 @@
|
|||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<!-- <service-->
|
||||||
android:name=".services.AppListenerService"
|
<!-- android:name=".services.AppListenerService"-->
|
||||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
<!-- android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"-->
|
||||||
android:exported="true">
|
<!-- android:exported="true">-->
|
||||||
<intent-filter>
|
<!-- <intent-filter>-->
|
||||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
<!-- <action android:name="android.accessibilityservice.AccessibilityService" />-->
|
||||||
</intent-filter>
|
<!-- </intent-filter>-->
|
||||||
<meta-data
|
<!-- <meta-data-->
|
||||||
android:name="android.accessibilityservice"
|
<!-- android:name="android.accessibilityservice"-->
|
||||||
android:resource="@xml/app_listener_service_config" />
|
<!-- android:resource="@xml/app_listener_service_config" />-->
|
||||||
</service>
|
<!-- </service>-->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receivers.BootReceiver"
|
android:name=".receivers.BootReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -3,52 +3,61 @@ cmake_minimum_required(VERSION 3.22.1)
|
|||||||
project("l2c_fcr_hook")
|
project("l2c_fcr_hook")
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
|
||||||
add_library(l2c_fcr_hook SHARED
|
option(IS_XPOSED "Build Xposed components" OFF)
|
||||||
l2c_fcr_hook.cpp
|
|
||||||
|
|
||||||
xz/xz_crc32.c
|
add_library(bluetooth_socket SHARED
|
||||||
xz/xz_crc64.c
|
bluetooth_socket.cpp
|
||||||
xz/xz_sha256.c
|
|
||||||
xz/xz_dec_stream.c
|
|
||||||
xz/xz_dec_lzma2.c
|
|
||||||
xz/xz_dec_bcj.c
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(socket_private_constructor SHARED
|
target_compile_options(bluetooth_socket PRIVATE
|
||||||
socket_private_constructor.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(l2c_fcr_hook PRIVATE
|
|
||||||
xz
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_definitions(l2c_fcr_hook PRIVATE
|
|
||||||
XZ_DEC_X86
|
|
||||||
XZ_DEC_ARM
|
|
||||||
XZ_DEC_ARMTHUMB
|
|
||||||
XZ_DEC_ARM64
|
|
||||||
XZ_DEC_ANY_CHECK
|
|
||||||
XZ_USE_CRC64
|
|
||||||
XZ_USE_SHA256
|
|
||||||
XZ_DEC_CONCATENATED
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_options(socket_private_constructor PRIVATE
|
|
||||||
-O2
|
-O2
|
||||||
-fvisibility=hidden
|
-fvisibility=hidden
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_options(socket_private_constructor PRIVATE
|
target_link_options(bluetooth_socket PRIVATE
|
||||||
-Wl,--strip-all
|
-Wl,--strip-all
|
||||||
-Wl,--gc-sections
|
-Wl,--gc-sections
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(l2c_fcr_hook
|
target_link_libraries(bluetooth_socket
|
||||||
android
|
android
|
||||||
log
|
log
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(socket_private_constructor
|
if(IS_XPOSED)
|
||||||
android
|
|
||||||
log
|
set(XPOSED_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../xposed/cpp)
|
||||||
)
|
|
||||||
|
add_library(l2c_fcr_hook SHARED
|
||||||
|
${XPOSED_SRC_DIR}/l2c_fcr_hook.cpp
|
||||||
|
|
||||||
|
${XPOSED_SRC_DIR}/xz/xz_crc32.c
|
||||||
|
${XPOSED_SRC_DIR}/xz/xz_crc64.c
|
||||||
|
${XPOSED_SRC_DIR}/xz/xz_sha256.c
|
||||||
|
${XPOSED_SRC_DIR}/xz/xz_dec_stream.c
|
||||||
|
${XPOSED_SRC_DIR}/xz/xz_dec_lzma2.c
|
||||||
|
${XPOSED_SRC_DIR}/xz/xz_dec_bcj.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(l2c_fcr_hook PRIVATE
|
||||||
|
${XPOSED_SRC_DIR}
|
||||||
|
${XPOSED_SRC_DIR}/xz
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(l2c_fcr_hook PRIVATE
|
||||||
|
XZ_DEC_X86
|
||||||
|
XZ_DEC_ARM
|
||||||
|
XZ_DEC_ARMTHUMB
|
||||||
|
XZ_DEC_ARM64
|
||||||
|
XZ_DEC_ANY_CHECK
|
||||||
|
XZ_USE_CRC64
|
||||||
|
XZ_USE_SHA256
|
||||||
|
XZ_DEC_CONCATENATED
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(l2c_fcr_hook
|
||||||
|
android
|
||||||
|
log
|
||||||
|
)
|
||||||
|
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
|
// import me.kavishdevar.librepods.screens.Onboarding
|
||||||
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
@@ -27,13 +29,11 @@ import android.content.Context
|
|||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@@ -73,7 +73,6 @@ import androidx.compose.material3.Card
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -88,7 +87,6 @@ import androidx.compose.ui.draw.scale
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
import androidx.compose.ui.graphics.drawscope.rotate
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
@@ -103,6 +101,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
@@ -112,9 +111,13 @@ import com.google.accompanist.permissions.isGranted
|
|||||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
|
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||||
|
import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||||
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
|
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.AdaptiveStrengthScreen
|
import me.kavishdevar.librepods.screens.AdaptiveStrengthScreen
|
||||||
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
||||||
@@ -126,30 +129,33 @@ import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
|
|||||||
import me.kavishdevar.librepods.screens.HearingAidScreen
|
import me.kavishdevar.librepods.screens.HearingAidScreen
|
||||||
import me.kavishdevar.librepods.screens.HearingProtectionScreen
|
import me.kavishdevar.librepods.screens.HearingProtectionScreen
|
||||||
import me.kavishdevar.librepods.screens.LongPress
|
import me.kavishdevar.librepods.screens.LongPress
|
||||||
// import me.kavishdevar.librepods.screens.Onboarding
|
|
||||||
import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen
|
import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen
|
||||||
import me.kavishdevar.librepods.screens.RenameScreen
|
import me.kavishdevar.librepods.screens.RenameScreen
|
||||||
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
|
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
|
||||||
import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
|
import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
|
||||||
import me.kavishdevar.librepods.screens.VersionScreen
|
import me.kavishdevar.librepods.screens.VersionScreen
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.isSupported
|
||||||
import kotlin.io.encoding.Base64
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
|
import me.kavishdevar.librepods.viewmodel.AppSettingsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
lateinit var serviceConnection: ServiceConnection
|
lateinit var serviceConnection: ServiceConnection
|
||||||
lateinit var connectionStatusReceiver: BroadcastReceiver
|
lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("l2c_fcr_hook")
|
if (BuildConfig.FLAVOR == "xposed") {
|
||||||
|
System.loadLibrary("l2c_fcr_hook")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -159,8 +165,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
Main()
|
Main()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIncomingIntent(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -195,69 +199,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
setIntent(intent)
|
|
||||||
handleIncomingIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleIncomingIntent(intent: Intent) {
|
|
||||||
val data: Uri? = intent.data
|
|
||||||
|
|
||||||
if (data != null && data.scheme == "librepods") {
|
|
||||||
when (data.host) {
|
|
||||||
"add-magic-keys" -> {
|
|
||||||
val queryParams = data.queryParameterNames
|
|
||||||
queryParams.forEach { param ->
|
|
||||||
val value = data.getQueryParameter(param)
|
|
||||||
Log.d("LibrePods", "Parameter: $param = $value")
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAddMagicKeys(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAddMagicKeys(uri: Uri) {
|
|
||||||
val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
|
||||||
|
|
||||||
val irkHex = uri.getQueryParameter("irk")
|
|
||||||
val encKeyHex = uri.getQueryParameter("enc_key")
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (irkHex != null && validateHexInput(irkHex)) {
|
|
||||||
val irkBytes = hexStringToByteArray(irkHex)
|
|
||||||
val irkBase64 = Base64.encode(irkBytes)
|
|
||||||
sharedPreferences.edit {putString("IRK", irkBase64)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encKeyHex != null && validateHexInput(encKeyHex)) {
|
|
||||||
val encKeyBytes = hexStringToByteArray(encKeyHex)
|
|
||||||
val encKeyBase64 = Base64.encode(encKeyBytes)
|
|
||||||
sharedPreferences.edit { putString("ENC_KEY", encKeyBase64)}
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(this, "Magic keys added successfully!", Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, "Error processing magic keys: ${e.message}", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateHexInput(input: String): Boolean {
|
|
||||||
val hexPattern = Regex("^[0-9a-fA-F]{32}$")
|
|
||||||
return hexPattern.matches(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hexStringToByteArray(hex: String): ByteArray {
|
|
||||||
val result = ByteArray(16)
|
|
||||||
for (i in 0 until 16) {
|
|
||||||
val hexByte = hex.substring(i * 2, i * 2 + 2)
|
|
||||||
result[i] = hexByte.toInt(16).toByte()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -265,12 +206,34 @@ class MainActivity : ComponentActivity() {
|
|||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main() {
|
fun Main() {
|
||||||
|
if (!isSupported()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Not supported. Device Info: BUILD_ID: ${Build.ID} SDK_INT_FULL: ${Build.VERSION.SDK_INT_FULL}, MANUFACTURER: ${Build.MANUFACTURER}.\nCheck out the repository for more info.",
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val isConnected = remember { mutableStateOf(false) }
|
val isConnected = remember { mutableStateOf(false) }
|
||||||
val isRemotelyConnected = remember { mutableStateOf(false) }
|
|
||||||
// val hookAvailable = RadareOffsetFinder(LocalContext.current).isHookOffsetAvailable()
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
||||||
val overlaySkipped = remember { mutableStateOf(context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("overlay_permission_skipped", false)) }
|
val overlaySkipped = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
.getBoolean("overlay_permission_skipped", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BillingManager.provider = BillingProviderFactory.create(context)
|
||||||
|
|
||||||
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -297,23 +260,33 @@ fun Main() {
|
|||||||
val permissionState = rememberMultiplePermissionsState(
|
val permissionState = rememberMultiplePermissionsState(
|
||||||
permissions = allPermissions
|
permissions = allPermissions
|
||||||
)
|
)
|
||||||
|
|
||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
|
|
||||||
|
val viewModel = remember(airPodsService.value) {
|
||||||
|
airPodsService.value?.let { service ->
|
||||||
|
AirPodsViewModel(
|
||||||
|
service = service,
|
||||||
|
sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE),
|
||||||
|
controlRepo = ControlCommandRepository(service.aacpManager),
|
||||||
|
appContext = context.applicationContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
canDrawOverlays = Settings.canDrawOverlays(context)
|
canDrawOverlays = Settings.canDrawOverlays(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permissionState.allPermissionsGranted && (canDrawOverlays || overlaySkipped.value)) {
|
if (permissionState.allPermissionsGranted && (canDrawOverlays || overlaySkipped.value)) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
Box (
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
) {
|
||||||
){
|
|
||||||
val backButtonBackdrop = rememberLayerBackdrop()
|
val backButtonBackdrop = rememberLayerBackdrop()
|
||||||
Box (
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7))
|
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7))
|
||||||
@@ -321,129 +294,125 @@ fun Main() {
|
|||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "settings", // if (hookAvailable) "settings" else "onboarding",
|
startDestination = "settings",
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { it },
|
initialOffsetX = { it }, animationSpec = tween(durationMillis = 300)
|
||||||
animationSpec = tween(durationMillis = 300)
|
)
|
||||||
) // + fadeIn(animationSpec = tween(durationMillis = 300))
|
|
||||||
},
|
},
|
||||||
exitTransition = {
|
exitTransition = {
|
||||||
slideOutHorizontally(
|
slideOutHorizontally(
|
||||||
targetOffsetX = { -it/4 },
|
targetOffsetX = { -it / 4 }, animationSpec = tween(durationMillis = 300)
|
||||||
animationSpec = tween(durationMillis = 300)
|
)
|
||||||
) // + fadeOut(animationSpec = tween(durationMillis = 150))
|
|
||||||
},
|
},
|
||||||
popEnterTransition = {
|
popEnterTransition = {
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { -it/4 },
|
initialOffsetX = { -it / 4 },
|
||||||
animationSpec = tween(durationMillis = 300)
|
animationSpec = tween(durationMillis = 300)
|
||||||
) // + fadeIn(animationSpec = tween(durationMillis = 300))
|
)
|
||||||
},
|
},
|
||||||
popExitTransition = {
|
popExitTransition = {
|
||||||
slideOutHorizontally(
|
slideOutHorizontally(
|
||||||
targetOffsetX = { it },
|
targetOffsetX = { it }, animationSpec = tween(durationMillis = 300)
|
||||||
animationSpec = tween(durationMillis = 300)
|
)
|
||||||
) // + fadeOut(animationSpec = tween(durationMillis = 150))
|
}) {
|
||||||
}
|
|
||||||
) {
|
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
if (airPodsService.value != null) {
|
if (viewModel != null) AirPodsSettingsScreen(viewModel, navController)
|
||||||
AirPodsSettingsScreen(
|
|
||||||
dev = airPodsService.value?.device,
|
|
||||||
service = airPodsService.value!!,
|
|
||||||
navController = navController,
|
|
||||||
isConnected = isConnected.value,
|
|
||||||
isRemotelyConnected = isRemotelyConnected.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable("debug") {
|
composable("debug") {
|
||||||
DebugScreen(navController = navController)
|
DebugScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable("long_press/{bud}") { navBackStackEntry ->
|
composable("long_press/{bud}") { navBackStackEntry ->
|
||||||
LongPress(
|
if (viewModel != null) LongPress(
|
||||||
navController = navController,
|
viewModel = viewModel,
|
||||||
name = navBackStackEntry.arguments?.getString("bud")!!
|
name = navBackStackEntry.arguments?.getString("bud")!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("rename") {
|
composable("rename") {
|
||||||
RenameScreen(navController)
|
if (viewModel != null) RenameScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable("app_settings") {
|
composable("app_settings") {
|
||||||
AppSettingsScreen(navController)
|
val appSettingsViewModel: AppSettingsViewModel = viewModel()
|
||||||
}
|
AppSettingsScreen(navController, appSettingsViewModel)
|
||||||
composable("troubleshooting") {
|
|
||||||
TroubleshootingScreen(navController)
|
|
||||||
}
|
}
|
||||||
|
// composable("troubleshooting") {
|
||||||
|
// TroubleshootingScreen(navController)
|
||||||
|
// }
|
||||||
composable("head_tracking") {
|
composable("head_tracking") {
|
||||||
HeadTrackingScreen()
|
if (viewModel != null) HeadTrackingScreen(viewModel)
|
||||||
}
|
}
|
||||||
/*composable("onboarding") {
|
|
||||||
Onboarding(navController, context)
|
|
||||||
}*/
|
|
||||||
composable("accessibility") {
|
composable("accessibility") {
|
||||||
AccessibilitySettingsScreen(navController)
|
if (viewModel != null) AccessibilitySettingsScreen(viewModel, navController)
|
||||||
}
|
}
|
||||||
composable("transparency_customization") {
|
composable("transparency_customization") {
|
||||||
TransparencySettingsScreen(navController)
|
if (viewModel != null) TransparencySettingsScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable("hearing_aid") {
|
composable("hearing_aid") {
|
||||||
HearingAidScreen(navController)
|
if (viewModel != null) HearingAidScreen(viewModel, navController)
|
||||||
}
|
}
|
||||||
composable("hearing_aid_adjustments") {
|
composable("hearing_aid_adjustments") {
|
||||||
HearingAidAdjustmentsScreen(navController)
|
if (viewModel != null) HearingAidAdjustmentsScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable("adaptive_strength") {
|
composable("adaptive_strength") {
|
||||||
AdaptiveStrengthScreen(navController)
|
if (viewModel != null) AdaptiveStrengthScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable("camera_control") {
|
composable("camera_control") {
|
||||||
CameraControlScreen(navController)
|
if (viewModel != null) CameraControlScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable("open_source_licenses") {
|
composable("open_source_licenses") {
|
||||||
OpenSourceLicensesScreen(navController)
|
OpenSourceLicensesScreen(navController)
|
||||||
}
|
}
|
||||||
composable("update_hearing_test") {
|
composable("update_hearing_test") {
|
||||||
UpdateHearingTestScreen(navController)
|
if (viewModel != null) UpdateHearingTestScreen()
|
||||||
}
|
}
|
||||||
composable("version_info") {
|
composable("version_info") {
|
||||||
VersionScreen(navController)
|
if (viewModel != null) VersionScreen(viewModel)
|
||||||
}
|
}
|
||||||
composable("hearing_protection") {
|
composable("hearing_protection") {
|
||||||
HearingProtectionScreen(navController)
|
if (viewModel != null) HearingProtectionScreen(viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val showBackButton = remember{ mutableStateOf(false) }
|
val showBackButton = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(navController) {
|
LaunchedEffect(navController) {
|
||||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||||
showBackButton.value = destination.route != "settings" // && destination.route != "onboarding"
|
showBackButton.value =
|
||||||
Log.d("MainActivity", "Navigated to ${destination.route}, showBackButton: ${showBackButton.value}")
|
destination.route != "settings" // && destination.route != "onboarding"
|
||||||
|
Log.d(
|
||||||
|
"MainActivity",
|
||||||
|
"Navigated to ${destination.route}, showBackButton: ${showBackButton.value}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showBackButton.value,
|
visible = showBackButton.value,
|
||||||
enter = fadeIn(animationSpec = tween()) + scaleIn(initialScale = 0f, animationSpec = tween()),
|
enter = fadeIn(animationSpec = tween()) + scaleIn(
|
||||||
exit = fadeOut(animationSpec = tween()) + scaleOut(targetScale = 0.5f, animationSpec = tween(100)),
|
initialScale = 0f,
|
||||||
|
animationSpec = tween()
|
||||||
|
),
|
||||||
|
exit = fadeOut(animationSpec = tween()) + scaleOut(
|
||||||
|
targetScale = 0.5f,
|
||||||
|
animationSpec = tween(100)
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.padding(
|
.padding(
|
||||||
start = 8.dp,
|
start = 8.dp, top = (LocalWindowInfo.current.containerSize.width * 0.05f).dp
|
||||||
top = (LocalWindowInfo.current.containerSize.width * 0.05f).dp
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
StyledIconButton(
|
StyledIconButton(
|
||||||
onClick = { navController.popBackStack() },
|
onClick = { navController.popBackStack() },
|
||||||
icon = "",
|
icon = "",
|
||||||
darkMode = isSystemInDarkTheme(),
|
backdrop = backButtonBackdrop
|
||||||
backdrop = backButtonBackdrop
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.startForegroundService(Intent(context, AirPodsService::class.java))
|
||||||
|
|
||||||
serviceConnection = remember {
|
serviceConnection = remember {
|
||||||
object : ServiceConnection {
|
object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
@@ -457,17 +426,20 @@ fun Main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
|
context.bindService(
|
||||||
|
Intent(context, AirPodsService::class.java),
|
||||||
|
serviceConnection,
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
|
||||||
if (airPodsService.value?.isConnectedLocally == true) {
|
if (airPodsService.value?.isConnected() == true) {
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PermissionsScreen(
|
PermissionsScreen(
|
||||||
permissionState = permissionState,
|
permissionState = permissionState,
|
||||||
canDrawOverlays = canDrawOverlays,
|
canDrawOverlays = canDrawOverlays,
|
||||||
onOverlaySettingsReturn = { canDrawOverlays = Settings.canDrawOverlays(context) }
|
onOverlaySettingsReturn = { canDrawOverlays = Settings.canDrawOverlays(context) })
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,13 +462,9 @@ fun PermissionsScreen(
|
|||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
|
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
|
||||||
val pulseScale by infiniteTransition.animateFloat(
|
val pulseScale by infiniteTransition.animateFloat(
|
||||||
initialValue = 1f,
|
initialValue = 1f, targetValue = 1.05f, animationSpec = infiniteRepeatable(
|
||||||
targetValue = 1.05f,
|
animation = tween(1000), repeatMode = RepeatMode.Reverse
|
||||||
animationSpec = infiniteRepeatable(
|
), label = "pulse scale"
|
||||||
animation = tween(1000),
|
|
||||||
repeatMode = RepeatMode.Reverse
|
|
||||||
),
|
|
||||||
label = "pulse scale"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -504,18 +472,15 @@ fun PermissionsScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(180.dp),
|
.height(180.dp), contentAlignment = Alignment.Center
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "\uDBC2\uDEB7",
|
text = "\uDBC2\uDEB7", style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 48.sp,
|
fontSize = 48.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -551,29 +516,25 @@ fun PermissionsScreen(
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Permission Required",
|
text = "Permission Required", style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
color = textColor,
|
color = textColor,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
),
|
), modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.permissions_required),
|
text = stringResource(R.string.permissions_required), style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
color = textColor.copy(alpha = 0.7f),
|
color = textColor.copy(alpha = 0.7f),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
),
|
), modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
@@ -746,8 +707,7 @@ fun PermissionCard(
|
|||||||
if (isGranted) accentColor.copy(alpha = 0.15f) else Color.Gray.copy(
|
if (isGranted) accentColor.copy(alpha = 0.15f) else Color.Gray.copy(
|
||||||
alpha = 0.15f
|
alpha = 0.15f
|
||||||
)
|
)
|
||||||
),
|
), contentAlignment = Alignment.Center
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
@@ -763,8 +723,7 @@ fun PermissionCard(
|
|||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title, style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -773,8 +732,7 @@ fun PermissionCard(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = description,
|
text = description, style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -791,11 +749,8 @@ fun PermissionCard(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (isGranted) "✓" else "!",
|
text = if (isGranted) "✓" else "!", style = TextStyle(
|
||||||
style = TextStyle(
|
fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color.White
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.kavishdevar.librepods.billing
|
||||||
|
|
||||||
|
object BillingManager {
|
||||||
|
lateinit var provider: BillingProvider
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.billing
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface BillingProvider {
|
||||||
|
val isPremium: StateFlow<Boolean>
|
||||||
|
|
||||||
|
fun purchase(activity: Activity)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.billing
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
|
|
||||||
|
object BillingProviderFactory {
|
||||||
|
|
||||||
|
fun create(context: Context): BillingProvider {
|
||||||
|
return if (BuildConfig.PLAY_BUILD) {
|
||||||
|
PlayBillingProvider(context)
|
||||||
|
} else {
|
||||||
|
FOSSBillingProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.billing
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class FOSSBillingProvider : BillingProvider {
|
||||||
|
private val _isPremium = MutableStateFlow(true)
|
||||||
|
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||||
|
|
||||||
|
override fun purchase(activity: Activity) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.billing
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||||
|
import com.android.billingclient.api.BillingClient
|
||||||
|
import com.android.billingclient.api.BillingClientStateListener
|
||||||
|
import com.android.billingclient.api.BillingFlowParams
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
import com.android.billingclient.api.PendingPurchasesParams
|
||||||
|
import com.android.billingclient.api.ProductDetails
|
||||||
|
import com.android.billingclient.api.Purchase
|
||||||
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
|
import com.android.billingclient.api.QueryPurchasesParams
|
||||||
|
import com.android.billingclient.api.acknowledgePurchase
|
||||||
|
import com.android.billingclient.api.queryProductDetails
|
||||||
|
import com.android.billingclient.api.queryPurchasesAsync
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
const val TAG = "PlayBillingProvider"
|
||||||
|
|
||||||
|
private const val PREMIUM_PRODUCT_ID = "librepods.advanced_features.v2"
|
||||||
|
|
||||||
|
class PlayBillingProvider(
|
||||||
|
context: Context
|
||||||
|
) : BillingProvider, PurchasesUpdatedListener {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
private val _isPremium = MutableStateFlow(false)
|
||||||
|
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||||
|
|
||||||
|
private var productDetails: ProductDetails? = null
|
||||||
|
|
||||||
|
private val billingClient = BillingClient.newBuilder(context)
|
||||||
|
.setListener(this)
|
||||||
|
.enablePendingPurchases(
|
||||||
|
PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
init {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connect() {
|
||||||
|
billingClient.startConnection(object : BillingClientStateListener {
|
||||||
|
override fun onBillingSetupFinished(result: BillingResult) {
|
||||||
|
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
|
scope.launch {
|
||||||
|
queryProductDetails()
|
||||||
|
queryExistingPurchases()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Billing setup failed: ${result.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBillingServiceDisconnected() {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryProductDetails() {
|
||||||
|
val params = QueryProductDetailsParams.newBuilder()
|
||||||
|
.setProductList(
|
||||||
|
listOf(
|
||||||
|
QueryProductDetailsParams.Product.newBuilder()
|
||||||
|
.setProductId(PREMIUM_PRODUCT_ID)
|
||||||
|
.setProductType(BillingClient.ProductType.INAPP)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
|
||||||
|
val result = billingClient.queryProductDetails(params)
|
||||||
|
if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
|
productDetails = result.productDetailsList?.firstOrNull()
|
||||||
|
Log.d(TAG, "Product loaded: ${productDetails?.name}")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "queryProductDetails failed: ${result.billingResult.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryExistingPurchases() {
|
||||||
|
val result = billingClient.queryPurchasesAsync(
|
||||||
|
QueryPurchasesParams.newBuilder()
|
||||||
|
.setProductType(BillingClient.ProductType.INAPP)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
processPurchases(result.purchasesList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun purchase(activity: Activity) {
|
||||||
|
val details = productDetails ?: run {
|
||||||
|
Log.e(TAG, "Product details not loaded yet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val billingFlowParams = BillingFlowParams.newBuilder()
|
||||||
|
.setProductDetailsParamsList(
|
||||||
|
listOf(
|
||||||
|
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||||
|
.setProductDetails(details)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
|
||||||
|
val result = billingClient.launchBillingFlow(activity, billingFlowParams)
|
||||||
|
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "launchBillingFlow failed: ${result.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
|
||||||
|
when (result.responseCode) {
|
||||||
|
BillingClient.BillingResponseCode.OK -> purchases?.let { processPurchases(it) }
|
||||||
|
BillingClient.BillingResponseCode.USER_CANCELED -> Log.d(TAG, "User cancelled")
|
||||||
|
else -> Log.w(TAG, "Purchase error ${result.responseCode}: ${result.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processPurchases(purchases: List<Purchase>) {
|
||||||
|
val hasPremium = purchases.any {
|
||||||
|
it.products.contains(PREMIUM_PRODUCT_ID) &&
|
||||||
|
it.purchaseState == Purchase.PurchaseState.PURCHASED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// val purchase = purchases.find {
|
||||||
|
// it.products.contains(PREMIUM_PRODUCT_ID) && it.purchaseState == Purchase.PurchaseState.PURCHASED
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (purchase != null) {
|
||||||
|
// val consumeParams = ConsumeParams.newBuilder()
|
||||||
|
// .setPurchaseToken(purchase.purchaseToken)
|
||||||
|
// .build()
|
||||||
|
// scope.launch {
|
||||||
|
// billingClient.consumeAsync(consumeParams) { _, _ ->}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
_isPremium.value = hasPremium
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
purchases
|
||||||
|
.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged }
|
||||||
|
.forEach { acknowledge(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun acknowledge(purchase: Purchase) {
|
||||||
|
val params = AcknowledgePurchaseParams.newBuilder()
|
||||||
|
.setPurchaseToken(purchase.purchaseToken)
|
||||||
|
.build()
|
||||||
|
val result = billingClient.acknowledgePurchase(params)
|
||||||
|
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||||
|
Log.e(TAG, "Acknowledgement failed: ${result.debugMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,8 +22,8 @@ package me.kavishdevar.librepods.composables
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -34,35 +34,35 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutCard(navController: NavController) {
|
fun AboutCard(
|
||||||
|
navController: NavController,
|
||||||
|
modelName: String,
|
||||||
|
actualModel: String,
|
||||||
|
serialNumbers: List<String>,
|
||||||
|
version: String?
|
||||||
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val service = ServiceManager.getService()
|
|
||||||
if (service == null) return
|
|
||||||
val airpodsInstance = service.airpodsInstance
|
|
||||||
if (airpodsInstance == null) return
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -108,7 +108,7 @@ fun AboutCard(navController: NavController) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = airpodsInstance.model.displayName,
|
text = modelName,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
|
||||||
@@ -137,7 +137,7 @@ fun AboutCard(navController: NavController) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = airpodsInstance.actualModelNumber,
|
text = actualModel,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
|
||||||
@@ -152,11 +152,11 @@ fun AboutCard(navController: NavController) {
|
|||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
)
|
)
|
||||||
val serialNumbers = listOf(
|
val serialNumbers = listOf(
|
||||||
airpodsInstance.serialNumber?: "",
|
serialNumbers[0],
|
||||||
" ${airpodsInstance.leftSerialNumber}",
|
" ${serialNumbers[1]}",
|
||||||
" ${airpodsInstance.rightSerialNumber}"
|
" ${serialNumbers[2]}"
|
||||||
)
|
)
|
||||||
val serialNumber = remember { mutableStateOf(0) }
|
val serialNumber = remember { mutableIntStateOf(0) }
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -172,7 +172,7 @@ fun AboutCard(navController: NavController) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = serialNumbers[serialNumber.value],
|
text = serialNumbers[serialNumber.intValue],
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.8f),
|
||||||
@@ -183,7 +183,7 @@ fun AboutCard(navController: NavController) {
|
|||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null
|
indication = null
|
||||||
) {
|
) {
|
||||||
serialNumber.value = (serialNumber.value + 1) % serialNumbers.size
|
serialNumber.intValue = (serialNumber.intValue + 1) % serialNumbers.size
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ fun AboutCard(navController: NavController) {
|
|||||||
to = "version_info",
|
to = "version_info",
|
||||||
navController = navController,
|
navController = navController,
|
||||||
name = stringResource(R.string.version),
|
name = stringResource(R.string.version),
|
||||||
currentState = airpodsInstance.version3,
|
currentState = version,
|
||||||
independent = false,
|
independent = false,
|
||||||
height = rowHeight.value + 32.dp
|
height = rowHeight.value + 32.dp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,25 +42,32 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
|
||||||
import me.kavishdevar.librepods.utils.Capability
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettings(navController: NavController) {
|
fun AudioSettings(
|
||||||
|
navController: NavController,
|
||||||
|
adaptiveVolumeCapability: Boolean,
|
||||||
|
conversationalAwarenessCapability: Boolean,
|
||||||
|
loudSoundReductionCapability: Boolean,
|
||||||
|
adaptiveAudioCapability: Boolean,
|
||||||
|
|
||||||
|
adaptiveVolumeChecked: Boolean,
|
||||||
|
onAdaptiveVolumeCheckedChange: (Boolean) -> Unit,
|
||||||
|
|
||||||
|
conversationalAwarenessChecked: Boolean,
|
||||||
|
onConversationalAwarenessCheckedChange: (Boolean) -> Unit,
|
||||||
|
|
||||||
|
loudSoundReductionChecked: Boolean,
|
||||||
|
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
|
||||||
|
|
||||||
|
isXposed: Boolean,
|
||||||
|
isPremium: Boolean
|
||||||
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val service = ServiceManager.getService()
|
|
||||||
if (service == null) return
|
if (!adaptiveVolumeCapability && !conversationalAwarenessCapability && !loudSoundReductionCapability && !adaptiveAudioCapability) {
|
||||||
val airpodsInstance = service.airpodsInstance
|
|
||||||
if (airpodsInstance == null) return
|
|
||||||
if (!airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME) &&
|
|
||||||
!airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS) &&
|
|
||||||
!airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) &&
|
|
||||||
!airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -88,12 +95,14 @@ fun AudioSettings(navController: NavController) {
|
|||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_VOLUME)) {
|
if (adaptiveVolumeCapability) {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.personalized_volume),
|
label = stringResource(R.string.personalized_volume),
|
||||||
description = stringResource(R.string.personalized_volume_description),
|
description = stringResource(R.string.personalized_volume_description),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
independent = false,
|
||||||
independent = false
|
checked = adaptiveVolumeChecked,
|
||||||
|
onCheckedChange = onAdaptiveVolumeCheckedChange,
|
||||||
|
enabled = isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -104,12 +113,14 @@ fun AudioSettings(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (airpodsInstance.model.capabilities.contains(Capability.CONVERSATION_AWARENESS)) {
|
if (conversationalAwarenessCapability) {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.conversational_awareness),
|
label = stringResource(R.string.conversational_awareness),
|
||||||
description = stringResource(R.string.conversational_awareness_description),
|
description = stringResource(R.string.conversational_awareness_description),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
independent = false,
|
||||||
independent = false
|
checked = conversationalAwarenessChecked,
|
||||||
|
onCheckedChange = onConversationalAwarenessCheckedChange,
|
||||||
|
enabled = isPremium
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -119,12 +130,13 @@ fun AudioSettings(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (airpodsInstance.model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
|
if (loudSoundReductionCapability && isXposed){
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.loud_sound_reduction),
|
label = stringResource(R.string.loud_sound_reduction),
|
||||||
description = stringResource(R.string.loud_sound_reduction_description),
|
description = stringResource(R.string.loud_sound_reduction_description),
|
||||||
attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
|
independent = false,
|
||||||
independent = false
|
checked = loudSoundReductionChecked,
|
||||||
|
onCheckedChange = onLoudSoundReductionCheckedChange
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -134,7 +146,7 @@ fun AudioSettings(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (airpodsInstance.model.capabilities.contains(Capability.ADAPTIVE_AUDIO)) {
|
if (adaptiveAudioCapability) {
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "adaptive_strength",
|
to = "adaptive_strength",
|
||||||
name = stringResource(R.string.adaptive_audio),
|
name = stringResource(R.string.adaptive_audio),
|
||||||
@@ -148,5 +160,19 @@ fun AudioSettings(navController: NavController) {
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettingsPreview() {
|
fun AudioSettingsPreview() {
|
||||||
AudioSettings(rememberNavController())
|
AudioSettings(
|
||||||
|
navController = rememberNavController(),
|
||||||
|
adaptiveVolumeCapability = true,
|
||||||
|
conversationalAwarenessCapability = true,
|
||||||
|
loudSoundReductionCapability = true,
|
||||||
|
adaptiveAudioCapability = true,
|
||||||
|
adaptiveVolumeChecked = true,
|
||||||
|
onAdaptiveVolumeCheckedChange = { },
|
||||||
|
conversationalAwarenessChecked = true,
|
||||||
|
onConversationalAwarenessCheckedChange = { },
|
||||||
|
loudSoundReductionChecked = true,
|
||||||
|
onLoudSoundReductionCheckedChange = { },
|
||||||
|
isXposed = true,
|
||||||
|
isPremium = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -39,169 +33,101 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
|
||||||
import me.kavishdevar.librepods.constants.Battery
|
import me.kavishdevar.librepods.constants.Battery
|
||||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
import me.kavishdevar.librepods.constants.BatteryComponent
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
fun BatteryView(
|
||||||
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
batteryList: List<Battery>,
|
||||||
|
budsRes: Int,
|
||||||
|
caseRes: Int
|
||||||
|
) {
|
||||||
|
val left = batteryList.find { it.component == BatteryComponent.LEFT }
|
||||||
|
val right = batteryList.find { it.component == BatteryComponent.RIGHT }
|
||||||
|
val case = batteryList.find { it.component == BatteryComponent.CASE }
|
||||||
|
|
||||||
val previousBatteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action == AirPodsNotifications.BATTERY_DATA) {
|
|
||||||
batteryStatus.value =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
intent.getParcelableArrayListExtra("data", Battery::class.java)
|
|
||||||
} else {
|
|
||||||
intent.getParcelableArrayListExtra("data")
|
|
||||||
}?.toList() ?: listOf()
|
|
||||||
}
|
|
||||||
else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
|
|
||||||
try {
|
|
||||||
context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
catch (_: IllegalArgumentException) {
|
|
||||||
Log.wtf("BatteryReceiver", "Receiver already unregistered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
LaunchedEffect(context) {
|
|
||||||
val batteryIntentFilter = IntentFilter()
|
|
||||||
.apply {
|
|
||||||
addAction(AirPodsNotifications.BATTERY_DATA)
|
|
||||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.registerReceiver(
|
|
||||||
batteryReceiver,
|
|
||||||
batteryIntentFilter,
|
|
||||||
Context.RECEIVER_EXPORTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previousBatteryStatus.value = batteryStatus.value
|
|
||||||
batteryStatus.value = service.getBattery()
|
|
||||||
|
|
||||||
if (preview) {
|
|
||||||
batteryStatus.value = listOf(
|
|
||||||
Battery(BatteryComponent.LEFT, 100, BatteryStatus.NOT_CHARGING),
|
|
||||||
Battery(BatteryComponent.RIGHT, 94, BatteryStatus.CHARGING),
|
|
||||||
Battery(BatteryComponent.CASE, 40, BatteryStatus.CHARGING)
|
|
||||||
)
|
|
||||||
previousBatteryStatus.value = batteryStatus.value
|
|
||||||
}
|
|
||||||
|
|
||||||
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
|
||||||
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
|
||||||
val case = batteryStatus.value.find { it.component == BatteryComponent.CASE }
|
|
||||||
val leftLevel = left?.level ?: 0
|
val leftLevel = left?.level ?: 0
|
||||||
val rightLevel = right?.level ?: 0
|
val rightLevel = right?.level ?: 0
|
||||||
val caseLevel = case?.level ?: 0
|
val caseLevel = case?.level ?: 0
|
||||||
val leftCharging = left?.status == BatteryStatus.CHARGING || left?.status == BatteryStatus.OPTIMIZED_CHARGING
|
|
||||||
val rightCharging = right?.status == BatteryStatus.CHARGING || right?.status == BatteryStatus.OPTIMIZED_CHARGING
|
|
||||||
val caseCharging = case?.status == BatteryStatus.CHARGING || case?.status == BatteryStatus.OPTIMIZED_CHARGING
|
|
||||||
|
|
||||||
val prevLeft = previousBatteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
val leftCharging = left?.status == BatteryStatus.CHARGING ||
|
||||||
val prevRight = previousBatteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
left?.status == BatteryStatus.OPTIMIZED_CHARGING
|
||||||
val prevCase = previousBatteryStatus.value.find { it.component == BatteryComponent.CASE }
|
|
||||||
val prevLeftCharging = prevLeft?.status == BatteryStatus.CHARGING
|
val rightCharging = right?.status == BatteryStatus.CHARGING ||
|
||||||
val prevRightCharging = prevRight?.status == BatteryStatus.CHARGING
|
right?.status == BatteryStatus.OPTIMIZED_CHARGING
|
||||||
val prevCaseCharging = prevCase?.status == BatteryStatus.CHARGING
|
|
||||||
|
val caseCharging = case?.status == BatteryStatus.CHARGING ||
|
||||||
|
case?.status == BatteryStatus.OPTIMIZED_CHARGING
|
||||||
|
|
||||||
val singleDisplayed = remember { mutableStateOf(false) }
|
val singleDisplayed = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val airpodsInstance = service.airpodsInstance
|
|
||||||
if (airpodsInstance == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val budsRes = airpodsInstance.model.budsRes
|
|
||||||
val caseRes = airpodsInstance.model.caseRes
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Column (
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(0.5f),
|
||||||
.fillMaxWidth(0.5f),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Image (
|
Image(
|
||||||
bitmap = ImageBitmap.imageResource(budsRes),
|
bitmap = ImageBitmap.imageResource(budsRes),
|
||||||
contentDescription = stringResource(R.string.buds),
|
contentDescription = stringResource(R.string.buds),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
leftCharging == rightCharging &&
|
leftCharging == rightCharging &&
|
||||||
(leftLevel - rightLevel) in -3..3
|
(leftLevel - rightLevel) in -3..3
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
leftLevel.coerceAtMost(rightLevel),
|
leftLevel.coerceAtMost(rightLevel),
|
||||||
leftCharging,
|
leftCharging
|
||||||
previousCharging = (prevLeftCharging && prevRightCharging)
|
|
||||||
)
|
)
|
||||||
singleDisplayed.value = true
|
singleDisplayed.value = true
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
singleDisplayed.value = false
|
singleDisplayed.value = false
|
||||||
Row (
|
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
|
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
leftLevel,
|
leftLevel,
|
||||||
leftCharging,
|
leftCharging,
|
||||||
"\uDBC6\uDCE5",
|
"\uDBC6\uDCE5"
|
||||||
previousCharging = prevLeftCharging
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (leftLevel > 0 && rightLevel > 0)
|
|
||||||
{
|
if (leftLevel > 0 && rightLevel > 0) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED)
|
|
||||||
{
|
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
rightLevel,
|
rightLevel,
|
||||||
rightCharging,
|
rightCharging,
|
||||||
"\uDBC6\uDCE8",
|
"\uDBC6\uDCE8"
|
||||||
previousCharging = prevRightCharging
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column (
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
@@ -211,14 +137,14 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
)
|
)
|
||||||
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
|
|
||||||
BatteryIndicator(
|
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
|
||||||
caseLevel,
|
BatteryIndicator(
|
||||||
caseCharging,
|
caseLevel,
|
||||||
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else "",
|
caseCharging,
|
||||||
previousCharging = prevCaseCharging
|
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,10 +152,23 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryViewPreview() {
|
fun BatteryViewPreview() {
|
||||||
|
val fakeBattery = listOf(
|
||||||
|
Battery(BatteryComponent.LEFT, 85, BatteryStatus.CHARGING),
|
||||||
|
Battery(BatteryComponent.RIGHT, 40, BatteryStatus.CHARGING),
|
||||||
|
Battery(BatteryComponent.CASE, 60, BatteryStatus.NOT_CHARGING)
|
||||||
|
)
|
||||||
|
|
||||||
val bg = if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)
|
val bg = if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.background(bg)
|
modifier = Modifier
|
||||||
|
.background(bg)
|
||||||
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
BatteryView(AirPodsService(), preview = true)
|
BatteryView(
|
||||||
|
batteryList = fakeBattery,
|
||||||
|
budsRes = R.drawable.airpods_pro_2_buds,
|
||||||
|
caseRes = R.drawable.airpods_pro_2_case
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-76
@@ -36,7 +36,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -56,19 +55,20 @@ import androidx.compose.ui.text.TextStyle
|
|||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
fun CallControlSettings(hazeState: HazeState) {
|
fun CallControlSettings(
|
||||||
|
hazeState: HazeState,
|
||||||
|
flipped: Boolean,
|
||||||
|
onCallControlValueChanged: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
@@ -93,24 +93,9 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
val callControlEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG
|
|
||||||
}?.value ?: byteArrayOf(0x00, 0x03)
|
|
||||||
|
|
||||||
val pressOnceText = stringResource(R.string.press_once)
|
val pressOnceText = stringResource(R.string.press_once)
|
||||||
val pressTwiceText = stringResource(R.string.press_twice)
|
val pressTwiceText = stringResource(R.string.press_twice)
|
||||||
|
|
||||||
var flipped by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
callControlEnabledValue.contentEquals(
|
|
||||||
byteArrayOf(
|
|
||||||
0x00,
|
|
||||||
0x02
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var singlePressAction by remember { mutableStateOf(if (flipped) pressTwiceText else pressOnceText) }
|
var singlePressAction by remember { mutableStateOf(if (flipped) pressTwiceText else pressOnceText) }
|
||||||
var doublePressAction by remember { mutableStateOf(if (flipped) pressOnceText else pressTwiceText) }
|
var doublePressAction by remember { mutableStateOf(if (flipped) pressOnceText else pressTwiceText) }
|
||||||
|
|
||||||
@@ -128,35 +113,6 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
var parentHoveredIndexDouble by remember { mutableStateOf<Int?>(null) }
|
var parentHoveredIndexDouble by remember { mutableStateOf<Int?>(null) }
|
||||||
var parentDragActiveDouble by remember { mutableStateOf(false) }
|
var parentDragActiveDouble by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
val listener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (AACPManager.Companion.ControlCommandIdentifiers.fromByte(controlCommand.identifier) ==
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG
|
|
||||||
) {
|
|
||||||
val newFlipped = controlCommand.value.contentEquals(byteArrayOf(0x00, 0x02))
|
|
||||||
flipped = newFlipped
|
|
||||||
singlePressAction = if (newFlipped) pressTwiceText else pressOnceText
|
|
||||||
doublePressAction = if (newFlipped) pressOnceText else pressTwiceText
|
|
||||||
Log.d(
|
|
||||||
"CallControlSettings",
|
|
||||||
"Control command received, flipped: $newFlipped"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG,
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.controlCommandListeners[AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG]?.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(flipped) {
|
LaunchedEffect(flipped) {
|
||||||
Log.d("CallControlSettings", "Call control flipped: $flipped")
|
Log.d("CallControlSettings", "Call control flipped: $flipped")
|
||||||
}
|
}
|
||||||
@@ -244,11 +200,8 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
if (option == pressOnceText) pressTwiceText else pressOnceText
|
if (option == pressOnceText) pressTwiceText else pressOnceText
|
||||||
showSinglePressDropdown = false
|
showSinglePressDropdown = false
|
||||||
lastDismissTimeSingle = System.currentTimeMillis()
|
lastDismissTimeSingle = System.currentTimeMillis()
|
||||||
val bytes = if (option == pressOnceText) byteArrayOf(
|
onCallControlValueChanged(option != pressOnceText)
|
||||||
0x00,
|
|
||||||
0x03
|
|
||||||
) else byteArrayOf(0x00, 0x02)
|
|
||||||
service.aacpManager.sendControlCommand(0x24, bytes)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parentHoveredIndexSingle = null
|
parentHoveredIndexSingle = null
|
||||||
@@ -313,11 +266,8 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
doublePressAction =
|
doublePressAction =
|
||||||
if (option == pressOnceText) pressTwiceText else pressOnceText
|
if (option == pressOnceText) pressTwiceText else pressOnceText
|
||||||
showSinglePressDropdown = false
|
showSinglePressDropdown = false
|
||||||
val bytes = if (option == pressOnceText) byteArrayOf(
|
val flipped = option != pressOnceText
|
||||||
0x00,
|
onCallControlValueChanged(flipped)
|
||||||
0x03
|
|
||||||
) else byteArrayOf(0x00, 0x02)
|
|
||||||
service.aacpManager.sendControlCommand(0x24, bytes)
|
|
||||||
},
|
},
|
||||||
hazeState = hazeState
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
@@ -379,11 +329,8 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
if (option == pressOnceText) pressTwiceText else pressOnceText
|
if (option == pressOnceText) pressTwiceText else pressOnceText
|
||||||
showDoublePressDropdown = false
|
showDoublePressDropdown = false
|
||||||
lastDismissTimeDouble = System.currentTimeMillis()
|
lastDismissTimeDouble = System.currentTimeMillis()
|
||||||
val bytes = if (option == pressOnceText) byteArrayOf(
|
val flipped = option == pressOnceText
|
||||||
0x00,
|
onCallControlValueChanged (flipped)
|
||||||
0x02
|
|
||||||
) else byteArrayOf(0x00, 0x03)
|
|
||||||
service.aacpManager.sendControlCommand(0x24, bytes)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parentHoveredIndexDouble = null
|
parentHoveredIndexDouble = null
|
||||||
@@ -448,11 +395,8 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
singlePressAction =
|
singlePressAction =
|
||||||
if (option == pressOnceText) pressTwiceText else pressOnceText
|
if (option == pressOnceText) pressTwiceText else pressOnceText
|
||||||
showDoublePressDropdown = false
|
showDoublePressDropdown = false
|
||||||
val bytes = if (option == pressOnceText) byteArrayOf(
|
val flipped = option == pressOnceText
|
||||||
0x00,
|
onCallControlValueChanged(flipped)
|
||||||
0x02
|
|
||||||
) else byteArrayOf(0x00, 0x03)
|
|
||||||
service.aacpManager.sendControlCommand(0x24, bytes)
|
|
||||||
},
|
},
|
||||||
hazeState = hazeState
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
@@ -461,10 +405,3 @@ fun CallControlSettings(hazeState: HazeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun CallControlSettingsPreview() {
|
|
||||||
CallControlSettings(HazeState())
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-19
@@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.Context.MODE_PRIVATE
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,16 +30,18 @@ import androidx.compose.material3.HorizontalDivider
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionSettings() {
|
fun ConnectionSettings(
|
||||||
|
automaticEarDetectionEnabled: Boolean,
|
||||||
|
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
|
||||||
|
automaticConnectionEnabled: Boolean,
|
||||||
|
onAutomaticConnectionChanged: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
@@ -52,10 +53,9 @@ fun ConnectionSettings() {
|
|||||||
) {
|
) {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.ear_detection),
|
label = stringResource(R.string.ear_detection),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
independent = false,
|
||||||
sharedPreferenceKey = "automatic_ear_detection",
|
checked = automaticEarDetectionEnabled,
|
||||||
sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE),
|
onCheckedChange = onAutomaticEarDetectionChanged
|
||||||
independent = false
|
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -67,16 +67,9 @@ fun ConnectionSettings() {
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.automatically_connect),
|
label = stringResource(R.string.automatically_connect),
|
||||||
description = stringResource(R.string.automatically_connect_description),
|
description = stringResource(R.string.automatically_connect_description),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
independent = false,
|
||||||
sharedPreferenceKey = "automatic_connection_ctrl_cmd",
|
checked = automaticConnectionEnabled,
|
||||||
sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE),
|
onCheckedChange = onAutomaticConnectionChanged
|
||||||
independent = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ConnectionSettingsPreview() {
|
|
||||||
ConnectionSettings()
|
|
||||||
}
|
|
||||||
|
|||||||
+60
-54
@@ -40,70 +40,76 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.Capability
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingHealthSettings(navController: NavController) {
|
fun HearingHealthSettings(
|
||||||
val service = ServiceManager.getService()
|
navController: NavController,
|
||||||
if (service == null) return
|
hasPPECapability: Boolean,
|
||||||
val airpodsInstance = service.airpodsInstance
|
hasHearingAidCapability: Boolean,
|
||||||
if (airpodsInstance == null) return
|
isXposed: Boolean
|
||||||
if (airpodsInstance.model.capabilities.contains(Capability.HEARING_AID)) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
val shouldShowHearingAid = hasHearingAidCapability && isXposed
|
||||||
|
|
||||||
if (airpodsInstance.model.capabilities.contains(Capability.PPE)) {
|
if (hasPPECapability && shouldShowHearingAid) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
){
|
){
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.hearing_health),
|
text = stringResource(R.string.hearing_health),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f)
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(28.dp))
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
|
||||||
.padding(top = 2.dp)
|
|
||||||
) {
|
|
||||||
NavigationButton(
|
|
||||||
to = "hearing_protection",
|
|
||||||
name = stringResource(R.string.hearing_protection),
|
|
||||||
navController = navController,
|
|
||||||
independent = false
|
|
||||||
)
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||||
|
.padding(top = 2.dp)
|
||||||
|
) {
|
||||||
|
NavigationButton(
|
||||||
|
to = "hearing_protection",
|
||||||
|
name = stringResource(R.string.hearing_protection),
|
||||||
|
navController = navController,
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
NavigationButton(
|
|
||||||
to = "hearing_aid",
|
|
||||||
name = stringResource(R.string.hearing_aid),
|
|
||||||
navController = navController,
|
|
||||||
independent = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "hearing_aid",
|
to = "hearing_aid",
|
||||||
name = stringResource(R.string.hearing_aid),
|
name = stringResource(R.string.hearing_aid),
|
||||||
navController = navController
|
navController = navController,
|
||||||
|
independent = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (shouldShowHearingAid) {
|
||||||
|
NavigationButton(
|
||||||
|
to = "hearing_aid",
|
||||||
|
name = stringResource(R.string.hearing_aid),
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
} else if (hasPPECapability) {
|
||||||
|
NavigationButton(
|
||||||
|
to = "hearing_protection",
|
||||||
|
name = stringResource(R.string.hearing_protection),
|
||||||
|
title = stringResource(R.string.hearing_health),
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-41
@@ -35,8 +35,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -54,19 +52,21 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
fun MicrophoneSettings(hazeState: HazeState) {
|
fun MicrophoneSettings(
|
||||||
|
hazeState: HazeState,
|
||||||
|
micModeValue: Byte,
|
||||||
|
onMicModeValueChanged: (Byte) -> Unit
|
||||||
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
@@ -77,11 +77,6 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
|||||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
val micModeValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE
|
|
||||||
}?.value?.get(0) ?: 0x00.toByte()
|
|
||||||
|
|
||||||
var selectedMode by remember {
|
var selectedMode by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
when (micModeValue) {
|
when (micModeValue) {
|
||||||
@@ -114,22 +109,6 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE,
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE,
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val itemHeightPx = with(density) { 48.dp.toPx() }
|
val itemHeightPx = with(density) { 48.dp.toPx() }
|
||||||
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
@@ -194,10 +173,11 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
|||||||
options[2] -> 0x02
|
options[2] -> 0x02
|
||||||
else -> 0x00
|
else -> 0x00
|
||||||
}
|
}
|
||||||
service.aacpManager.sendControlCommand(
|
// service.aacpManager.sendControlCommand(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
|
// AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
|
||||||
byteArrayOf(byteValue.toByte())
|
// byteArrayOf(byteValue.toByte())
|
||||||
)
|
// )
|
||||||
|
onMicModeValueChanged(byteValue.toByte())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parentHoveredIndex = null
|
parentHoveredIndex = null
|
||||||
@@ -277,10 +257,7 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
|||||||
microphoneAlwaysLeftText -> 0x02
|
microphoneAlwaysLeftText -> 0x02
|
||||||
else -> 0x00
|
else -> 0x00
|
||||||
}
|
}
|
||||||
service.aacpManager.sendControlCommand(
|
onMicModeValueChanged(byteValue.toByte())
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
|
|
||||||
byteArrayOf(byteValue.toByte())
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
hazeState = hazeState
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
@@ -288,10 +265,3 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun MicrophoneSettingsPreview() {
|
|
||||||
MicrophoneSettings(HazeState())
|
|
||||||
}
|
|
||||||
|
|||||||
+26
-90
@@ -21,11 +21,6 @@
|
|||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.SpringSpec
|
import androidx.compose.animation.core.SpringSpec
|
||||||
@@ -60,48 +55,28 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
|
||||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
import me.kavishdevar.librepods.constants.NoiseControlMode
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSettings(
|
fun NoiseControlSettings(
|
||||||
service: AirPodsService,
|
showOffListeningMode: Boolean,
|
||||||
|
noiseControlModeValue: Int,
|
||||||
|
onNoiseControlModeChanged: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val offListeningModeConfigValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0) != 2.toByte()
|
|
||||||
val offListeningMode = remember { mutableStateOf(offListeningModeConfigValue) }
|
|
||||||
|
|
||||||
val offListeningModeListener = object: AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
offListeningMode.value = controlCommand.value[0] != 2.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
|
||||||
offListeningModeListener
|
|
||||||
)
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
@@ -109,7 +84,6 @@ fun NoiseControlSettings(
|
|||||||
val selectedBackground = if (isDarkTheme) Color(0xBF5C5A5F) else Color(0xFFFFFFFF)
|
val selectedBackground = if (isDarkTheme) Color(0xBF5C5A5F) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
|
||||||
val noiseControlModeFromService = service.aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE)
|
|
||||||
|
|
||||||
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
|
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
|
||||||
|
|
||||||
@@ -117,10 +91,11 @@ fun NoiseControlSettings(
|
|||||||
val d2a = remember { mutableFloatStateOf(0f) }
|
val d2a = remember { mutableFloatStateOf(0f) }
|
||||||
val d3a = remember { mutableFloatStateOf(0f) }
|
val d3a = remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
// this function exists solely for the dividers, should get rid of it
|
||||||
fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) {
|
fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) {
|
||||||
val previousMode = noiseControlMode.value
|
val previousMode = noiseControlMode.value
|
||||||
|
|
||||||
val targetMode = if (!offListeningMode.value && mode == NoiseControlMode.OFF) {
|
val targetMode = if (!showOffListeningMode && mode == NoiseControlMode.OFF) {
|
||||||
NoiseControlMode.TRANSPARENCY
|
NoiseControlMode.TRANSPARENCY
|
||||||
} else {
|
} else {
|
||||||
mode
|
mode
|
||||||
@@ -128,9 +103,8 @@ fun NoiseControlSettings(
|
|||||||
|
|
||||||
noiseControlMode.value = targetMode
|
noiseControlMode.value = targetMode
|
||||||
|
|
||||||
if (!received && targetMode != previousMode) {
|
if (!received && targetMode != previousMode) onNoiseControlModeChanged(targetMode.ordinal + 1)
|
||||||
service.aacpManager.sendControlCommand(identifier = AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value, value = targetMode.ordinal + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (noiseControlMode.value) {
|
when (noiseControlMode.value) {
|
||||||
NoiseControlMode.NOISE_CANCELLATION -> {
|
NoiseControlMode.NOISE_CANCELLATION -> {
|
||||||
@@ -157,42 +131,11 @@ fun NoiseControlSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (noiseControlModeFromService != null) {
|
val index = (noiseControlModeValue - 1).coerceIn(0, NoiseControlMode.entries.size - 1)
|
||||||
val value = noiseControlModeFromService.value
|
noiseControlMode.value = NoiseControlMode.entries[index]
|
||||||
if (value.isNotEmpty()) {
|
|
||||||
val index = (value[0].toInt() - 1).coerceIn(0, NoiseControlMode.entries.size - 1)
|
|
||||||
noiseControlMode.value = NoiseControlMode.entries[index]
|
|
||||||
|
|
||||||
onModeSelected(noiseControlMode.value, received = true)
|
onModeSelected(noiseControlMode.value, received = true)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val noiseControlReceiver = remember {
|
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action == AirPodsNotifications.ANC_DATA) {
|
|
||||||
noiseControlMode.value = NoiseControlMode.entries.toTypedArray()[intent.getIntExtra("data", 3) - 1]
|
|
||||||
onModeSelected(noiseControlMode.value, true)
|
|
||||||
} else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
|
|
||||||
try {
|
|
||||||
context.unregisterReceiver(this)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val noiseControlIntentFilter = IntentFilter().apply {
|
|
||||||
addAction(AirPodsNotifications.ANC_DATA)
|
|
||||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter)
|
|
||||||
}
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
@@ -207,14 +150,14 @@ fun NoiseControlSettings(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val buttonCount = if (offListeningMode.value) 4 else 3
|
val buttonCount = if (showOffListeningMode) 4 else 3
|
||||||
val buttonWidth = maxWidth / buttonCount
|
val buttonWidth = maxWidth / buttonCount
|
||||||
|
|
||||||
val isDragging = remember { mutableStateOf(false) }
|
val isDragging = remember { mutableStateOf(false) }
|
||||||
@@ -222,10 +165,10 @@ fun NoiseControlSettings(
|
|||||||
mutableFloatStateOf(
|
mutableFloatStateOf(
|
||||||
with(density) {
|
with(density) {
|
||||||
when(noiseControlMode.value) {
|
when(noiseControlMode.value) {
|
||||||
NoiseControlMode.OFF -> if (offListeningMode.value) 0f else buttonWidth.toPx()
|
NoiseControlMode.OFF -> if (showOffListeningMode) 0f else buttonWidth.toPx()
|
||||||
NoiseControlMode.TRANSPARENCY -> if (offListeningMode.value) buttonWidth.toPx() else 0f
|
NoiseControlMode.TRANSPARENCY -> if (showOffListeningMode) buttonWidth.toPx() else 0f
|
||||||
NoiseControlMode.ADAPTIVE -> if (offListeningMode.value) (buttonWidth * 2).toPx() else buttonWidth.toPx()
|
NoiseControlMode.ADAPTIVE -> if (showOffListeningMode) (buttonWidth * 2).toPx() else buttonWidth.toPx()
|
||||||
NoiseControlMode.NOISE_CANCELLATION -> if (offListeningMode.value) (buttonWidth * 3).toPx() else (buttonWidth * 2).toPx()
|
NoiseControlMode.NOISE_CANCELLATION -> if (showOffListeningMode) (buttonWidth * 3).toPx() else (buttonWidth * 2).toPx()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -238,10 +181,10 @@ fun NoiseControlSettings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val targetOffset = buttonWidth * when(noiseControlMode.value) {
|
val targetOffset = buttonWidth * when(noiseControlMode.value) {
|
||||||
NoiseControlMode.OFF -> if (offListeningMode.value) 0 else 1
|
NoiseControlMode.OFF -> if (showOffListeningMode) 0 else 1
|
||||||
NoiseControlMode.TRANSPARENCY -> if (offListeningMode.value) 1 else 0
|
NoiseControlMode.TRANSPARENCY -> if (showOffListeningMode) 1 else 0
|
||||||
NoiseControlMode.ADAPTIVE -> if (offListeningMode.value) 2 else 1
|
NoiseControlMode.ADAPTIVE -> if (showOffListeningMode) 2 else 1
|
||||||
NoiseControlMode.NOISE_CANCELLATION -> if (offListeningMode.value) 3 else 2
|
NoiseControlMode.NOISE_CANCELLATION -> if (showOffListeningMode) 3 else 2
|
||||||
}
|
}
|
||||||
|
|
||||||
val animatedOffset by animateFloatAsState(
|
val animatedOffset by animateFloatAsState(
|
||||||
@@ -264,7 +207,7 @@ fun NoiseControlSettings(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
if (offListeningMode.value) {
|
if (showOffListeningMode) {
|
||||||
NoiseControlButton(
|
NoiseControlButton(
|
||||||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||||||
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
||||||
@@ -337,13 +280,12 @@ fun NoiseControlSettings(
|
|||||||
val position = dragOffset / with(density) { buttonWidth.toPx() }
|
val position = dragOffset / with(density) { buttonWidth.toPx() }
|
||||||
val newIndex = position.roundToInt()
|
val newIndex = position.roundToInt()
|
||||||
val newMode = when(newIndex) {
|
val newMode = when(newIndex) {
|
||||||
0 -> if (offListeningMode.value) NoiseControlMode.OFF else NoiseControlMode.TRANSPARENCY
|
0 -> if (showOffListeningMode) NoiseControlMode.OFF else NoiseControlMode.TRANSPARENCY
|
||||||
1 -> if (offListeningMode.value) NoiseControlMode.TRANSPARENCY else NoiseControlMode.ADAPTIVE
|
1 -> if (showOffListeningMode) NoiseControlMode.TRANSPARENCY else NoiseControlMode.ADAPTIVE
|
||||||
2 -> if (offListeningMode.value) NoiseControlMode.ADAPTIVE else NoiseControlMode.NOISE_CANCELLATION
|
2 -> if (showOffListeningMode) NoiseControlMode.ADAPTIVE else NoiseControlMode.NOISE_CANCELLATION
|
||||||
3 -> NoiseControlMode.NOISE_CANCELLATION
|
3 -> NoiseControlMode.NOISE_CANCELLATION
|
||||||
else -> noiseControlMode.value // Keep current if index is invalid
|
else -> noiseControlMode.value // Keep current if index is invalid
|
||||||
}
|
}
|
||||||
// Call onModeSelected which now handles service call but not callback
|
|
||||||
onModeSelected(newMode)
|
onModeSelected(newMode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -361,7 +303,7 @@ fun NoiseControlSettings(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.zIndex(1f)
|
.zIndex(1f)
|
||||||
) {
|
) {
|
||||||
if (offListeningMode.value) {
|
if (showOffListeningMode) {
|
||||||
NoiseControlButton(
|
NoiseControlButton(
|
||||||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||||||
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
||||||
@@ -420,7 +362,7 @@ fun NoiseControlSettings(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 4.dp)
|
.padding(top = 4.dp)
|
||||||
) {
|
) {
|
||||||
if (offListeningMode.value) {
|
if (showOffListeningMode) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.off),
|
text = stringResource(R.string.off),
|
||||||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||||||
@@ -450,9 +392,3 @@ fun NoiseControlSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun NoiseControlSettingsPreview() {
|
|
||||||
NoiseControlSettings(AirPodsService())
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-21
@@ -18,15 +18,11 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -35,13 +31,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -49,24 +43,22 @@ import me.kavishdevar.librepods.R
|
|||||||
import me.kavishdevar.librepods.constants.StemAction
|
import me.kavishdevar.librepods.constants.StemAction
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PressAndHoldSettings(navController: NavController) {
|
fun PressAndHoldSettings(
|
||||||
|
navController: NavController,
|
||||||
|
leftAction: StemAction,
|
||||||
|
rightAction: StemAction
|
||||||
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val dividerColor = Color(0x40888888)
|
val dividerColor = Color(0x40888888)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val leftActionText = when (leftAction) {
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
val leftAction = sharedPreferences.getString("left_long_press_action", StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
|
||||||
val rightAction = sharedPreferences.getString("right_long_press_action", StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
|
||||||
|
|
||||||
val leftActionText = when (StemAction.valueOf(leftAction ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) {
|
|
||||||
StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control)
|
StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control)
|
||||||
StemAction.DIGITAL_ASSISTANT -> "Digital Assistant"
|
StemAction.DIGITAL_ASSISTANT -> "Digital Assistant"
|
||||||
else -> "INVALID!!"
|
else -> "INVALID!!"
|
||||||
}
|
}
|
||||||
|
|
||||||
val rightActionText = when (StemAction.valueOf(rightAction ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) {
|
val rightActionText = when (rightAction) {
|
||||||
StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control)
|
StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control)
|
||||||
StemAction.DIGITAL_ASSISTANT -> "Digital Assistant"
|
StemAction.DIGITAL_ASSISTANT -> "Digital Assistant"
|
||||||
else -> "INVALID!!"
|
else -> "INVALID!!"
|
||||||
@@ -114,9 +106,3 @@ fun PressAndHoldSettings(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
fun PressAndHoldSettingsPreview() {
|
|
||||||
PressAndHoldSettings(navController = NavController(LocalContext.current))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import androidx.compose.ui.util.lerp
|
|||||||
import com.kyant.backdrop.Backdrop
|
import com.kyant.backdrop.Backdrop
|
||||||
import com.kyant.backdrop.drawBackdrop
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import com.kyant.backdrop.effects.blur
|
import com.kyant.backdrop.effects.blur
|
||||||
import com.kyant.backdrop.effects.refraction
|
import com.kyant.backdrop.effects.lens
|
||||||
import com.kyant.backdrop.effects.vibrancy
|
import com.kyant.backdrop.effects.vibrancy
|
||||||
import com.kyant.backdrop.highlight.Highlight
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -146,7 +146,12 @@ half4 main(float2 coord) {
|
|||||||
effects = {
|
effects = {
|
||||||
vibrancy()
|
vibrancy()
|
||||||
blur(2f.dp.toPx())
|
blur(2f.dp.toPx())
|
||||||
refraction(12f.dp.toPx(), 24f.dp.toPx())
|
lens(
|
||||||
|
refractionHeight = 12f.dp.toPx(),
|
||||||
|
refractionAmount = 24f.dp.toPx(),
|
||||||
|
depthEffect = true,
|
||||||
|
chromaticAberration = true
|
||||||
|
)
|
||||||
},
|
},
|
||||||
layerBlock = {
|
layerBlock = {
|
||||||
val width = size.width
|
val width = size.width
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ import androidx.compose.ui.util.lerp
|
|||||||
import com.kyant.backdrop.backdrops.LayerBackdrop
|
import com.kyant.backdrop.backdrops.LayerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import com.kyant.backdrop.drawBackdrop
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import com.kyant.backdrop.effects.blur
|
import com.kyant.backdrop.effects.lens
|
||||||
import com.kyant.backdrop.effects.refractionWithDispersion
|
|
||||||
import com.kyant.backdrop.highlight.Highlight
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import com.kyant.backdrop.shadow.Shadow
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -78,13 +77,13 @@ import kotlin.math.tanh
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledIconButton(
|
fun StyledIconButton(
|
||||||
onClick: () -> Unit,
|
modifier: Modifier = Modifier,
|
||||||
icon: String,
|
icon: String,
|
||||||
darkMode: Boolean,
|
|
||||||
tint: Color = Color.Unspecified,
|
tint: Color = Color.Unspecified,
|
||||||
backdrop: LayerBackdrop = rememberLayerBackdrop(),
|
backdrop: LayerBackdrop = rememberLayerBackdrop(),
|
||||||
modifier: Modifier = Modifier,
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val darkMode = isSystemInDarkTheme()
|
||||||
val animationScope = rememberCoroutineScope()
|
val animationScope = rememberCoroutineScope()
|
||||||
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
|
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
|
||||||
@@ -218,8 +217,12 @@ half4 main(float2 coord) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
effects = {
|
effects = {
|
||||||
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
lens(
|
||||||
// blur(24f, TileMode.Decal)
|
refractionHeight = 6f.dp.toPx(),
|
||||||
|
refractionAmount = size.height / 2f,
|
||||||
|
depthEffect = true,
|
||||||
|
chromaticAberration = true
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.pointerInput(animationScope) {
|
.pointerInput(animationScope) {
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import dev.chrisbanes.haze.rememberHazeState
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledScaffold(
|
fun StyledScaffold(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -133,7 +132,6 @@ fun StyledScaffold(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledScaffold(
|
fun StyledScaffold(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -150,7 +148,6 @@ fun StyledScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledScaffold(
|
fun StyledScaffold(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
+12
-18
@@ -48,7 +48,6 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
@@ -59,19 +58,10 @@ data class SelectItem(
|
|||||||
val iconRes: Int? = null,
|
val iconRes: Int? = null,
|
||||||
val selected: Boolean,
|
val selected: Boolean,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
|
val visible: Boolean = true,
|
||||||
val enabled: Boolean = true
|
val enabled: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SelectItem2(
|
|
||||||
val name: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val iconRes: Int? = null,
|
|
||||||
val selected: () -> Boolean,
|
|
||||||
val onClick: () -> Unit,
|
|
||||||
val enabled: Boolean = true
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSelectList(
|
fun StyledSelectList(
|
||||||
items: List<SelectItem>,
|
items: List<SelectItem>,
|
||||||
@@ -87,18 +77,19 @@ fun StyledSelectList(
|
|||||||
.background(backgroundColor, RoundedCornerShape(28.dp)),
|
.background(backgroundColor, RoundedCornerShape(28.dp)),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
val visibleItems = items.filter { it.enabled }
|
val visibleItems = items.filter { it.visible }
|
||||||
visibleItems.forEachIndexed { index, item ->
|
visibleItems.forEachIndexed { index, item ->
|
||||||
val isFirst = index == 0
|
val isFirst = index == 0
|
||||||
val isLast = index == visibleItems.size - 1
|
val isLast = index == visibleItems.size - 1
|
||||||
val hasIcon = item.iconRes != null
|
val hasIcon = item.iconRes != null
|
||||||
|
|
||||||
val shape = when {
|
val shape = when {
|
||||||
|
isFirst && isLast -> RoundedCornerShape(28.dp)
|
||||||
isFirst -> RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
isFirst -> RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
||||||
isLast -> RoundedCornerShape(bottomStart = 28.dp, bottomEnd = 28.dp)
|
isLast -> RoundedCornerShape(bottomStart = 28.dp, bottomEnd = 28.dp)
|
||||||
else -> RoundedCornerShape(0.dp)
|
else -> RoundedCornerShape(0.dp)
|
||||||
}
|
}
|
||||||
var itemBackgroundColor by remember { mutableStateOf(backgroundColor) }
|
var itemBackgroundColor by remember { mutableStateOf(if (item.enabled) backgroundColor else if (isDarkTheme) Color(0x40050505) else Color(0x40D9D9D9)) }
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = itemBackgroundColor, animationSpec = tween(durationMillis = 500))
|
val animatedBackgroundColor by animateColorAsState(targetValue = itemBackgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -108,10 +99,13 @@ fun StyledSelectList(
|
|||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
itemBackgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
if (item.enabled) {
|
||||||
tryAwaitRelease()
|
itemBackgroundColor =
|
||||||
itemBackgroundColor = backgroundColor
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
item.onClick()
|
tryAwaitRelease()
|
||||||
|
itemBackgroundColor = backgroundColor
|
||||||
|
item.onClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -121,7 +115,7 @@ fun StyledSelectList(
|
|||||||
) {
|
) {
|
||||||
if (hasIcon) {
|
if (hasIcon) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(item.iconRes!!),
|
painter = painterResource(item.iconRes),
|
||||||
contentDescription = "Icon",
|
contentDescription = "Icon",
|
||||||
tint = Color(0xFF007AFF),
|
tint = Color(0xFF007AFF),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
@@ -43,7 +44,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableFloatState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
@@ -81,7 +81,7 @@ import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
|||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import com.kyant.backdrop.drawBackdrop
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import com.kyant.backdrop.effects.blur
|
import com.kyant.backdrop.effects.blur
|
||||||
import com.kyant.backdrop.effects.refractionWithDispersion
|
import com.kyant.backdrop.effects.lens
|
||||||
import com.kyant.backdrop.highlight.Highlight
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import com.kyant.backdrop.shadow.InnerShadow
|
import com.kyant.backdrop.shadow.InnerShadow
|
||||||
import com.kyant.backdrop.shadow.Shadow
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
@@ -203,10 +203,11 @@ class MomentumAnimation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnrememberedMutableState")
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSlider(
|
fun StyledSlider(
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
mutableFloatState: MutableFloatState,
|
value: Float,
|
||||||
onValueChange: (Float) -> Unit,
|
onValueChange: (Float) -> Unit,
|
||||||
valueRange: ClosedFloatingPointRange<Float>,
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
backdrop: Backdrop = rememberLayerBackdrop(),
|
backdrop: Backdrop = rememberLayerBackdrop(),
|
||||||
@@ -217,23 +218,26 @@ fun StyledSlider(
|
|||||||
startLabel: String? = null,
|
startLabel: String? = null,
|
||||||
endLabel: String? = null,
|
endLabel: String? = null,
|
||||||
independent: Boolean = false,
|
independent: Boolean = false,
|
||||||
description: String? = null
|
description: String? = null,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val isLightTheme = !isSystemInDarkTheme()
|
val isLightTheme = !isSystemInDarkTheme()
|
||||||
val accentColor =
|
|
||||||
if (isLightTheme) Color(0xFF0088FF)
|
|
||||||
else Color(0xFF0091FF)
|
|
||||||
val trackColor =
|
val trackColor =
|
||||||
if (isLightTheme) Color(0xFF787878).copy(0.2f)
|
if (isLightTheme) Color(0xFF787878).copy(0.2f)
|
||||||
else Color(0xFF787880).copy(0.36f)
|
else Color(0xFF787880).copy(0.36f)
|
||||||
|
val accentColor =
|
||||||
|
if (enabled) {
|
||||||
|
if (isLightTheme) Color(0xFF0088FF)
|
||||||
|
else Color(0xFF0091FF)
|
||||||
|
} else {
|
||||||
|
trackColor
|
||||||
|
}
|
||||||
val labelTextColor = if (isLightTheme) Color.Black else Color.White
|
val labelTextColor = if (isLightTheme) Color.Black else Color.White
|
||||||
|
|
||||||
val fraction by remember {
|
val fraction by derivedStateOf {
|
||||||
derivedStateOf {
|
((value - valueRange.start) / (valueRange.endInclusive - valueRange.start))
|
||||||
((mutableFloatState.floatValue - valueRange.start) / (valueRange.endInclusive - valueRange.start))
|
.fastCoerceIn(0f, 1f)
|
||||||
.fastCoerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val sliderBackdrop = rememberLayerBackdrop()
|
val sliderBackdrop = rememberLayerBackdrop()
|
||||||
@@ -427,71 +431,87 @@ fun StyledSlider(
|
|||||||
)
|
)
|
||||||
translationY = if (startLabel != null || endLabel != null) trackPositionState.floatValue + with(density) { 26.dp.toPx() } + size.height / 2f else trackPositionState.floatValue + with(density) { 8.dp.toPx() }
|
translationY = if (startLabel != null || endLabel != null) trackPositionState.floatValue + with(density) { 26.dp.toPx() } + size.height / 2f else trackPositionState.floatValue + with(density) { 8.dp.toPx() }
|
||||||
}
|
}
|
||||||
.draggable(
|
.then(
|
||||||
rememberDraggableState { delta ->
|
if (enabled) {
|
||||||
val trackWidth = trackWidthState.floatValue
|
Modifier
|
||||||
if (trackWidth > 0f) {
|
.draggable(
|
||||||
val targetFraction = fraction + delta / trackWidth
|
rememberDraggableState { delta ->
|
||||||
val targetValue =
|
val trackWidth = trackWidthState.floatValue
|
||||||
lerp(valueRange.start, valueRange.endInclusive, targetFraction)
|
if (trackWidth > 0f) {
|
||||||
.fastCoerceIn(valueRange.start, valueRange.endInclusive)
|
val targetFraction = fraction + delta / trackWidth
|
||||||
val snappedValue = if (snapPoints.isNotEmpty()) snapIfClose(
|
val targetValue =
|
||||||
targetValue,
|
lerp(
|
||||||
snapPoints,
|
valueRange.start,
|
||||||
snapThreshold
|
valueRange.endInclusive,
|
||||||
) else targetValue
|
targetFraction
|
||||||
onValueChange(snappedValue)
|
)
|
||||||
}
|
.fastCoerceIn(
|
||||||
},
|
valueRange.start,
|
||||||
Orientation.Horizontal,
|
valueRange.endInclusive
|
||||||
startDragImmediately = true,
|
)
|
||||||
onDragStarted = {
|
val snappedValue = if (snapPoints.isNotEmpty()) snapIfClose(
|
||||||
// Remove this block as momentumAnimation handles pressing
|
targetValue,
|
||||||
},
|
snapPoints,
|
||||||
onDragStopped = {
|
snapThreshold
|
||||||
// Remove this block as momentumAnimation handles pressing
|
) else targetValue
|
||||||
onValueChange((mutableFloatState.floatValue * 100).roundToInt() / 100f)
|
onValueChange(snappedValue)
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
.then(momentumAnimation.modifier)
|
Orientation.Horizontal,
|
||||||
.drawBackdrop(
|
startDragImmediately = true,
|
||||||
rememberCombinedBackdrop(backdrop, sliderBackdrop),
|
onDragStarted = {
|
||||||
{ RoundedCornerShape(28.dp) },
|
// Remove this block as momentumAnimation handles pressing
|
||||||
highlight = {
|
},
|
||||||
val progress = momentumAnimation.progress
|
onDragStopped = {
|
||||||
Highlight.Ambient.copy(alpha = progress)
|
// Remove this block as momentumAnimation handles pressing
|
||||||
},
|
onValueChange((value * 100).roundToInt() / 100f)
|
||||||
shadow = {
|
}
|
||||||
Shadow(
|
)
|
||||||
radius = 4f.dp,
|
.then(momentumAnimation.modifier)
|
||||||
color = Color.Black.copy(0.05f)
|
.drawBackdrop(
|
||||||
)
|
rememberCombinedBackdrop(backdrop, sliderBackdrop),
|
||||||
},
|
{ RoundedCornerShape(28.dp) },
|
||||||
innerShadow = {
|
highlight = {
|
||||||
val progress = momentumAnimation.progress
|
val progress = momentumAnimation.progress
|
||||||
InnerShadow(
|
Highlight.Ambient.copy(alpha = progress)
|
||||||
radius = 4f.dp * progress,
|
},
|
||||||
alpha = progress
|
shadow = {
|
||||||
)
|
Shadow(
|
||||||
},
|
radius = 4f.dp,
|
||||||
layerBlock = {
|
color = Color.Black.copy(0.05f)
|
||||||
scaleX = momentumAnimation.scaleX
|
)
|
||||||
scaleY = momentumAnimation.scaleY
|
},
|
||||||
val velocity = momentumAnimation.velocity / 5000f
|
innerShadow = {
|
||||||
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.15f, 0.15f)
|
val progress = momentumAnimation.progress
|
||||||
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.15f, 0.15f)
|
InnerShadow(
|
||||||
},
|
radius = 4f.dp * progress,
|
||||||
onDrawSurface = {
|
alpha = progress
|
||||||
val progress = momentumAnimation.progress
|
)
|
||||||
drawRect(Color.White.copy(alpha = 1f - progress))
|
},
|
||||||
},
|
layerBlock = {
|
||||||
effects = {
|
scaleX = momentumAnimation.scaleX
|
||||||
val progress = momentumAnimation.progress
|
scaleY = momentumAnimation.scaleY
|
||||||
blur(8f.dp.toPx() * (1f - progress))
|
val velocity = momentumAnimation.velocity / 5000f
|
||||||
refractionWithDispersion(
|
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.15f, 0.15f)
|
||||||
height = 6f.dp.toPx() * progress,
|
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.15f, 0.15f)
|
||||||
amount = size.height / 2f * progress
|
},
|
||||||
)
|
onDrawSurface = {
|
||||||
|
val progress = momentumAnimation.progress
|
||||||
|
drawRect(Color.White.copy(alpha = 1f - progress))
|
||||||
|
},
|
||||||
|
effects = {
|
||||||
|
val progress = momentumAnimation.progress
|
||||||
|
blur(8f.dp.toPx() * (1f - progress))
|
||||||
|
lens(
|
||||||
|
refractionHeight = 6f.dp.toPx() * progress,
|
||||||
|
refractionAmount = size.height / 2f * progress,
|
||||||
|
depthEffect = true,
|
||||||
|
chromaticAberration = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier.background(trackColor, RoundedCornerShape(28.dp))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.size(40f.dp, 24f.dp)
|
.size(40f.dp, 24f.dp)
|
||||||
@@ -566,12 +586,13 @@ fun StyledSliderPreview() {
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
Box (
|
Column (
|
||||||
Modifier.align(Alignment.Center)
|
Modifier.align(Alignment.Center),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
mutableFloatState = a,
|
value = a.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
a.floatValue = it
|
a.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -582,6 +603,19 @@ fun StyledSliderPreview() {
|
|||||||
startIcon = "A",
|
startIcon = "A",
|
||||||
endIcon = "B",
|
endIcon = "B",
|
||||||
)
|
)
|
||||||
|
StyledSlider(
|
||||||
|
value = a.floatValue,
|
||||||
|
onValueChange = {
|
||||||
|
a.floatValue = it
|
||||||
|
},
|
||||||
|
valueRange = 0f..2f,
|
||||||
|
snapPoints = listOf(1f),
|
||||||
|
snapThreshold = 0.1f,
|
||||||
|
independent = true,
|
||||||
|
startIcon = "A",
|
||||||
|
endIcon = "B",
|
||||||
|
enabled = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.Animatable
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
@@ -68,7 +68,7 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
|||||||
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import com.kyant.backdrop.drawBackdrop
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import com.kyant.backdrop.effects.refractionWithDispersion
|
import com.kyant.backdrop.effects.lens
|
||||||
import com.kyant.backdrop.highlight.Highlight
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import com.kyant.backdrop.shadow.Shadow
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
@@ -100,22 +100,18 @@ fun StyledSwitch(
|
|||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val animationScope = rememberCoroutineScope()
|
val animationScope = rememberCoroutineScope()
|
||||||
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
val colorAnimationSpec = tween<Color>(200, easing = FastOutSlowInEasing)
|
|
||||||
val progressAnimation = remember { Animatable(0f) }
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
val innerShadowLayer = rememberGraphicsLayer().apply {
|
val innerShadowLayer = rememberGraphicsLayer().apply {
|
||||||
compositingStrategy = CompositingStrategy.Offscreen
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
}
|
}
|
||||||
val animatedTrackColor = remember { Animatable(if (checked) onColor else offColor) }
|
val targetColor = if (checked) onColor else offColor
|
||||||
|
val animatedTrackColor by animateColorAsState(targetColor)
|
||||||
val totalDrag = remember { mutableFloatStateOf(0f) }
|
val totalDrag = remember { mutableFloatStateOf(0f) }
|
||||||
val tapThreshold = 10f
|
val tapThreshold = 10f
|
||||||
val isFirstComposition = remember { mutableStateOf(true) }
|
val isFirstComposition = remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(checked) {
|
LaunchedEffect(checked) {
|
||||||
if (!isFirstComposition.value) {
|
if (!isFirstComposition.value) {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch {
|
|
||||||
val targetColor = if (checked) onColor else offColor
|
|
||||||
animatedTrackColor.animateTo(targetColor, colorAnimationSpec)
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
val targetFrac = if (checked) 1f else 0f
|
val targetFrac = if (checked) 1f else 0f
|
||||||
animatedFraction.animateTo(targetFrac, progressAnimationSpec)
|
animatedFraction.animateTo(targetFrac, progressAnimationSpec)
|
||||||
@@ -140,7 +136,7 @@ fun StyledSwitch(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layerBackdrop(switchBackdrop)
|
.layerBackdrop(switchBackdrop)
|
||||||
.clip(RoundedCornerShape(trackHeight / 2))
|
.clip(RoundedCornerShape(trackHeight / 2))
|
||||||
.background(animatedTrackColor.value)
|
.background(animatedTrackColor)
|
||||||
.width(trackWidth)
|
.width(trackWidth)
|
||||||
.height(trackHeight)
|
.height(trackHeight)
|
||||||
.onSizeChanged { trackWidthPx.floatValue = it.width.toFloat() }
|
.onSizeChanged { trackWidthPx.floatValue = it.width.toFloat() }
|
||||||
@@ -262,7 +258,12 @@ fun StyledSwitch(
|
|||||||
drawRect(Color.White.copy(1f - progress))
|
drawRect(Color.White.copy(1f - progress))
|
||||||
},
|
},
|
||||||
effects = {
|
effects = {
|
||||||
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
lens(
|
||||||
|
refractionHeight = 6f.dp.toPx(),
|
||||||
|
refractionAmount = size.height / 2f,
|
||||||
|
depthEffect = true,
|
||||||
|
chromaticAberration = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.width(thumbWidth)
|
.width(thumbWidth)
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -39,18 +37,15 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -58,11 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -70,32 +61,27 @@ fun StyledToggle(
|
|||||||
title: String? = null,
|
title: String? = null,
|
||||||
label: String,
|
label: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
checkedState: MutableState<Boolean> = remember { mutableStateOf(false) } ,
|
checked: Boolean = false,
|
||||||
sharedPreferenceKey: String? = null,
|
|
||||||
sharedPreferences: SharedPreferences? = null,
|
|
||||||
independent: Boolean = true,
|
independent: Boolean = true,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val currentChecked by rememberUpdatedState(checked)
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
var checked by checkedState
|
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
var backgroundColor by remember {
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
mutableStateOf(
|
||||||
if (sharedPreferenceKey != null && sharedPreferences != null) {
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
checked = sharedPreferences.getBoolean(sharedPreferenceKey, checked)
|
)
|
||||||
}
|
|
||||||
fun cb() {
|
|
||||||
if (sharedPreferences != null) {
|
|
||||||
if (sharedPreferenceKey == null) {
|
|
||||||
Log.e("StyledToggle", "SharedPreferenceKey is null but SharedPreferences is provided.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
|
||||||
}
|
|
||||||
onCheckedChange?.invoke(checked)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val animatedBackgroundColor by animateColorAsState(
|
||||||
|
targetValue = backgroundColor,
|
||||||
|
animationSpec = tween(durationMillis = 500)
|
||||||
|
)
|
||||||
|
|
||||||
if (independent) {
|
if (independent) {
|
||||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
@@ -106,9 +92,15 @@ fun StyledToggle(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f)
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp)
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = 4.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(28.dp))
|
.background(animatedBackgroundColor, RoundedCornerShape(28.dp))
|
||||||
@@ -124,8 +116,7 @@ fun StyledToggle(
|
|||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
checked = !checked
|
onCheckedChange(!currentChecked)
|
||||||
cb()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -148,24 +139,29 @@ fun StyledToggle(
|
|||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledSwitch(
|
StyledSwitch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
checked = it
|
onCheckedChange(it)
|
||||||
cb()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF000000)
|
||||||
|
else Color(0xFFF2F2F7)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = description,
|
text = description,
|
||||||
@@ -181,6 +177,7 @@ fun StyledToggle(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -203,8 +200,7 @@ fun StyledToggle(
|
|||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
checked = !checked
|
onCheckedChange(!currentChecked)
|
||||||
cb()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -223,7 +219,9 @@ fun StyledToggle(
|
|||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Text(
|
Text(
|
||||||
text = description,
|
text = description,
|
||||||
@@ -235,438 +233,13 @@ fun StyledToggle(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledSwitch(
|
StyledSwitch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
checked = it
|
onCheckedChange(it)
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StyledToggle(
|
|
||||||
title: String? = null,
|
|
||||||
label: String,
|
|
||||||
description: String? = null,
|
|
||||||
controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers,
|
|
||||||
independent: Boolean = true,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
sharedPreferenceKey: String? = null,
|
|
||||||
sharedPreferences: SharedPreferences? = null,
|
|
||||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val service = ServiceManager.getService() ?: return
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val checkedValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == controlCommandIdentifier
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var checked by remember { mutableStateOf(checkedValue == 1.toByte()) }
|
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
|
||||||
if (sharedPreferenceKey != null && sharedPreferences != null) {
|
|
||||||
checked = sharedPreferences.getBoolean(sharedPreferenceKey, checked)
|
|
||||||
}
|
|
||||||
fun cb() {
|
|
||||||
service.aacpManager.sendControlCommand(identifier = controlCommandIdentifier.value, value = checked)
|
|
||||||
if (sharedPreferences != null) {
|
|
||||||
if (sharedPreferenceKey == null) {
|
|
||||||
Log.e("StyledToggle", "SharedPreferenceKey is null but SharedPreferences is provided.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
|
||||||
}
|
|
||||||
onCheckedChange?.invoke(checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
val listener = remember {
|
|
||||||
object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == controlCommandIdentifier.value) {
|
|
||||||
Log.d("StyledToggle", "Received control command for $label: ${controlCommand.value}")
|
|
||||||
checked = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0) == 1.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(controlCommandIdentifier, listener)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(controlCommandIdentifier, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (independent) {
|
|
||||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
|
||||||
if (title != null) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = textColor.copy(alpha = 0.6f)
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(28.dp))
|
|
||||||
.padding(4.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
backgroundColor =
|
|
||||||
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
|
||||||
tryAwaitRelease()
|
|
||||||
backgroundColor =
|
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
if (enabled) {
|
|
||||||
checked = !checked
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(55.dp)
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
StyledSwitch(
|
|
||||||
checked = checked,
|
|
||||||
enabled = enabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
if (enabled) {
|
|
||||||
checked = it
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = textColor.copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(28.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(16.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
if (enabled) {
|
|
||||||
checked = !checked
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
if (description != null) {
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = checked,
|
|
||||||
enabled = enabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
if (enabled) {
|
|
||||||
checked = it
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StyledToggle(
|
|
||||||
title: String? = null,
|
|
||||||
label: String,
|
|
||||||
description: String? = null,
|
|
||||||
attHandle: ATTHandles,
|
|
||||||
independent: Boolean = true,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
sharedPreferenceKey: String? = null,
|
|
||||||
sharedPreferences: SharedPreferences? = null,
|
|
||||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: return
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val checkedValue = try {
|
|
||||||
attManager.read(attHandle).getOrNull(0)?.toInt()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("StyledToggle", "Error reading initial value for $label: ${e.message}")
|
|
||||||
null
|
|
||||||
} ?: 0
|
|
||||||
var checked by remember { mutableStateOf(checkedValue !=0) }
|
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
|
||||||
|
|
||||||
attManager.enableNotifications(attHandle)
|
|
||||||
|
|
||||||
if (sharedPreferenceKey != null && sharedPreferences != null) {
|
|
||||||
checked = sharedPreferences.getBoolean(sharedPreferenceKey, checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cb() {
|
|
||||||
if (sharedPreferences != null) {
|
|
||||||
if (sharedPreferenceKey == null) {
|
|
||||||
Log.e("StyledToggle", "SharedPreferenceKey is null but SharedPreferences is provided.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
|
||||||
}
|
|
||||||
onCheckedChange?.invoke(checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(checked) {
|
|
||||||
if (attManager.socket?.isConnected != true) return@LaunchedEffect
|
|
||||||
attManager.write(attHandle, if (checked) byteArrayOf(1) else byteArrayOf(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
val listener = remember {
|
|
||||||
object : (ByteArray) -> Unit {
|
|
||||||
override fun invoke(value: ByteArray) {
|
|
||||||
if (value.isNotEmpty()) {
|
|
||||||
checked = value[0].toInt() != 0
|
|
||||||
Log.d("StyledToggle", "Updated from notification for $label: enabled=$checked")
|
|
||||||
} else {
|
|
||||||
Log.w("StyledToggle", "Empty value in notification for $label")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
attManager.registerListener(attHandle, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
attManager.unregisterListener(attHandle, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (independent) {
|
|
||||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
|
||||||
if (title != null) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = textColor.copy(alpha = 0.6f)
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(28.dp))
|
|
||||||
.padding(4.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
backgroundColor =
|
|
||||||
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
|
||||||
tryAwaitRelease()
|
|
||||||
backgroundColor =
|
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
if (enabled) {
|
|
||||||
checked = !checked
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(55.dp)
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
StyledSwitch(
|
|
||||||
checked = checked,
|
|
||||||
enabled = enabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
if (enabled) {
|
|
||||||
checked = it
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = textColor.copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(28.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(16.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
if (enabled) {
|
|
||||||
checked = !checked
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
if (description != null) {
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = checked,
|
|
||||||
enabled = enabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
if (enabled) {
|
|
||||||
checked = it
|
|
||||||
cb()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -677,11 +250,11 @@ fun StyledToggle(
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledTogglePreview() {
|
fun StyledTogglePreview() {
|
||||||
val context = LocalContext.current
|
val checked = remember { mutableStateOf(false) }
|
||||||
val sharedPrefs = context.getSharedPreferences("preview", 0)
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = "Example Toggle",
|
label = "Example Toggle",
|
||||||
description = "This is an example description for the styled toggle.",
|
description = "This is an example description for the styled toggle.",
|
||||||
sharedPreferences = sharedPrefs
|
checked = checked.value,
|
||||||
|
onCheckedChange = { checked.value = !checked.value }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ class AirPodsNotifications {
|
|||||||
const val AIRPODS_DISCONNECTED = "me.kavishdevar.librepods.AIRPODS_DISCONNECTED"
|
const val AIRPODS_DISCONNECTED = "me.kavishdevar.librepods.AIRPODS_DISCONNECTED"
|
||||||
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTION_DETECTED"
|
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTION_DETECTED"
|
||||||
const val DISCONNECT_RECEIVERS = "me.kavishdevar.librepods.DISCONNECT_RECEIVERS"
|
const val DISCONNECT_RECEIVERS = "me.kavishdevar.librepods.DISCONNECT_RECEIVERS"
|
||||||
|
const val EQ_DATA = "me.kavishdevar.librepods.EQ_DATA"
|
||||||
|
const val AIRPODS_INFORMATION_UPDATED = "me.kavishdevar.librepods.AIRPODS_INFORMATION_UPDATED"
|
||||||
}
|
}
|
||||||
|
|
||||||
class EarDetection {
|
class EarDetection {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
|
||||||
|
class ControlCommandRepository(
|
||||||
|
private val aacpManager: AACPManager
|
||||||
|
) {
|
||||||
|
fun getValue(
|
||||||
|
identifier: AACPManager.Companion.ControlCommandIdentifiers
|
||||||
|
): ByteArray? {
|
||||||
|
return aacpManager.controlCommandStatusList
|
||||||
|
.find { it.identifier == identifier }
|
||||||
|
?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(
|
||||||
|
id: AACPManager.Companion.ControlCommandIdentifiers,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
aacpManager.sendControlCommand(id.value, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun observe(
|
||||||
|
identifier: AACPManager.Companion.ControlCommandIdentifiers,
|
||||||
|
onChange: (ByteArray) -> Unit
|
||||||
|
): AACPManager.ControlCommandListener {
|
||||||
|
|
||||||
|
val listener = object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
onChange(controlCommand.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aacpManager.registerControlCommandListener(identifier, listener)
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(
|
||||||
|
identifier: AACPManager.Companion.ControlCommandIdentifiers,
|
||||||
|
listener: AACPManager.ControlCommandListener
|
||||||
|
) {
|
||||||
|
aacpManager.unregisterControlCommandListener(identifier, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-195
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -39,10 +39,8 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -69,74 +67,35 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.composables.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.StyledDropdown
|
import me.kavishdevar.librepods.composables.StyledDropdown
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.Capability
|
import me.kavishdevar.librepods.utils.Capability
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
private var phoneMediaDebounceJob: Job? = null
|
//private var phoneMediaDebounceJob: Job? = null
|
||||||
private var toneVolumeDebounceJob: Job? = null
|
//private var toneVolumeDebounceJob: Job? = null
|
||||||
private const val TAG = "AccessibilitySettings"
|
//private const val TAG = "AccessibilitySettings"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccessibilitySettingsScreen(navController: NavController) {
|
fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavController) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
|
||||||
val isSdpOffsetAvailable = remember { mutableStateOf(false) } // always available rn, for testing without radare
|
|
||||||
// remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
val hearingAidEnabled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(1)?.toInt() == 1 && state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0)?.toInt() == 1
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
|
||||||
|
|
||||||
val capabilities = remember { ServiceManager.getService()?.airpodsInstance?.model?.capabilities ?: emptySet<Capability>() }
|
|
||||||
|
|
||||||
val hearingAidEnabled = remember { mutableStateOf(
|
|
||||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
|
|
||||||
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 0x01.toByte()
|
|
||||||
) }
|
|
||||||
|
|
||||||
val hearingAidListener = remember {
|
|
||||||
object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
|
|
||||||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
|
|
||||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
|
||||||
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
|
|
||||||
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
@@ -153,170 +112,73 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
|
|
||||||
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
// val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||||
val phoneEQEnabled = remember { mutableStateOf(false) }
|
// val phoneEQEnabled = remember { mutableStateOf(false) }
|
||||||
val mediaEQEnabled = remember { mutableStateOf(false) }
|
// val mediaEQEnabled = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val pressSpeedOptions = mapOf(
|
val pressSpeedOptions = mapOf(
|
||||||
0.toByte() to stringResource(R.string.default_option),
|
0.toByte() to stringResource(R.string.default_option),
|
||||||
1.toByte() to stringResource(R.string.slower),
|
1.toByte() to stringResource(R.string.slower),
|
||||||
2.toByte() to stringResource(R.string.slowest)
|
2.toByte() to stringResource(R.string.slowest)
|
||||||
)
|
)
|
||||||
val selectedPressSpeedValue =
|
|
||||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL }?.value?.takeIf { it.isNotEmpty() }
|
val selectedPressSpeedValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL]?.getOrNull(0)
|
||||||
?.get(0)
|
|
||||||
var selectedPressSpeed by remember {
|
var selectedPressSpeed by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0]
|
pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val selectedPressSpeedListener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
selectedPressSpeed = pressSpeedOptions[newValue] ?: pressSpeedOptions[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
aacpManager?.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
|
|
||||||
selectedPressSpeedListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
aacpManager?.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
|
|
||||||
selectedPressSpeedListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val pressAndHoldDurationOptions = mapOf(
|
val pressAndHoldDurationOptions = mapOf(
|
||||||
0.toByte() to stringResource(R.string.default_option),
|
0.toByte() to stringResource(R.string.default_option),
|
||||||
1.toByte() to stringResource(R.string.slower),
|
1.toByte() to stringResource(R.string.slower),
|
||||||
2.toByte() to stringResource(R.string.slowest)
|
2.toByte() to stringResource(R.string.slowest)
|
||||||
)
|
)
|
||||||
val selectedPressAndHoldDurationValue =
|
|
||||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL }?.value?.takeIf { it.isNotEmpty() }
|
val selectedPressAndHoldDurationValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL]?.getOrNull(0)
|
||||||
?.get(0)
|
|
||||||
var selectedPressAndHoldDuration by remember {
|
var selectedPressAndHoldDuration by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
pressAndHoldDurationOptions[selectedPressAndHoldDurationValue]
|
pressAndHoldDurationOptions[selectedPressAndHoldDurationValue]
|
||||||
?: pressAndHoldDurationOptions[0]
|
?: pressAndHoldDurationOptions[0]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val selectedPressAndHoldDurationListener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
selectedPressAndHoldDuration =
|
|
||||||
pressAndHoldDurationOptions[newValue] ?: pressAndHoldDurationOptions[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
aacpManager?.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
|
|
||||||
selectedPressAndHoldDurationListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
aacpManager?.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
|
|
||||||
selectedPressAndHoldDurationListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val volumeSwipeSpeedOptions = mapOf(
|
val volumeSwipeSpeedOptions = mapOf(
|
||||||
1.toByte() to stringResource(R.string.default_option),
|
1.toByte() to stringResource(R.string.default_option),
|
||||||
2.toByte() to stringResource(R.string.longer),
|
2.toByte() to stringResource(R.string.longer),
|
||||||
3.toByte() to stringResource(R.string.longest)
|
3.toByte() to stringResource(R.string.longest)
|
||||||
)
|
)
|
||||||
val selectedVolumeSwipeSpeedValue =
|
val selectedVolumeSwipeSpeedValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL]?.getOrNull(0)
|
||||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL }?.value?.takeIf { it.isNotEmpty() }
|
|
||||||
?.get(0)
|
|
||||||
var selectedVolumeSwipeSpeed by remember {
|
var selectedVolumeSwipeSpeed by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue]
|
volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue]
|
||||||
?: volumeSwipeSpeedOptions[1]
|
?: volumeSwipeSpeedOptions[1]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val selectedVolumeSwipeSpeedListener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
selectedVolumeSwipeSpeed =
|
|
||||||
volumeSwipeSpeedOptions[newValue] ?: volumeSwipeSpeedOptions[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
aacpManager?.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL,
|
|
||||||
selectedVolumeSwipeSpeedListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
aacpManager?.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL,
|
|
||||||
selectedVolumeSwipeSpeedListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
|
// LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
|
||||||
phoneMediaDebounceJob?.cancel()
|
// phoneMediaDebounceJob?.cancel()
|
||||||
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
// phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
delay(150)
|
// delay(150)
|
||||||
val manager = ServiceManager.getService()?.aacpManager
|
// val manager = ServiceManager.getService()?.aacpManager
|
||||||
if (manager == null) {
|
// if (manager == null) {
|
||||||
Log.w(TAG, "Cannot write EQ: AACPManager not available")
|
// Log.w(TAG, "Cannot write EQ: AACPManager not available")
|
||||||
return@launch
|
// return@launch
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte()
|
// val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte()
|
||||||
val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte()
|
// val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte()
|
||||||
Log.d(
|
// Log.d(
|
||||||
TAG,
|
// TAG,
|
||||||
"Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})"
|
// "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})"
|
||||||
)
|
// )
|
||||||
manager.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte)
|
// manager.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte)
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
Log.w(TAG, "Error sending phone/media EQ: ${e.message}")
|
// Log.w(TAG, "Error sending phone/media EQ: ${e.message}")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
val toneVolumeValue = remember { mutableFloatStateOf(
|
|
||||||
aacpManager?.controlCommandStatusList?.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)?.toFloat() ?: 75f
|
|
||||||
) }
|
|
||||||
LaunchedEffect(toneVolumeValue.floatValue) {
|
|
||||||
toneVolumeDebounceJob?.cancel()
|
|
||||||
toneVolumeDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
delay(150)
|
|
||||||
val manager = ServiceManager.getService()?.aacpManager
|
|
||||||
if (manager == null) {
|
|
||||||
Log.w(TAG, "Cannot write tone volume: AACPManager not available")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
manager.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME.value,
|
|
||||||
value = byteArrayOf(toneVolumeValue.floatValue.toInt().toByte(), 0x50.toByte())
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Error sending tone volume: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = stringResource(R.string.press_speed),
|
label = stringResource(R.string.press_speed),
|
||||||
@@ -325,8 +187,8 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
selectedOption = selectedPressSpeed?: stringResource(R.string.default_option),
|
selectedOption = selectedPressSpeed?: stringResource(R.string.default_option),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedPressSpeed = newValue
|
selectedPressSpeed = newValue
|
||||||
aacpManager?.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value,
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
|
||||||
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
||||||
?: 0.toByte()
|
?: 0.toByte()
|
||||||
)
|
)
|
||||||
@@ -343,8 +205,8 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
selectedOption = selectedPressAndHoldDuration?: stringResource(R.string.default_option),
|
selectedOption = selectedPressAndHoldDuration?: stringResource(R.string.default_option),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedPressAndHoldDuration = newValue
|
selectedPressAndHoldDuration = newValue
|
||||||
aacpManager?.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value,
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
|
||||||
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull()
|
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull()
|
||||||
?: 0.toByte()
|
?: 0.toByte()
|
||||||
)
|
)
|
||||||
@@ -358,19 +220,21 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
title = stringResource(R.string.noise_control),
|
title = stringResource(R.string.noise_control),
|
||||||
label = stringResource(R.string.noise_cancellation_single_airpod),
|
label = stringResource(R.string.noise_cancellation_single_airpod),
|
||||||
description = stringResource(R.string.noise_cancellation_single_airpod_description),
|
description = stringResource(R.string.noise_cancellation_single_airpod_description),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE,
|
|
||||||
independent = true,
|
independent = true,
|
||||||
|
checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE]?.getOrNull(0) == 0x01.toByte(),
|
||||||
|
onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)) {
|
if (state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && BuildConfig.FLAVOR == "xposed") {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.loud_sound_reduction),
|
label = stringResource(R.string.loud_sound_reduction),
|
||||||
description = stringResource(R.string.loud_sound_reduction_description),
|
description = stringResource(R.string.loud_sound_reduction_description),
|
||||||
attHandle = ATTHandles.LOUD_SOUND_REDUCTION
|
checked = viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)?.get(0) == 1.toByte(),
|
||||||
|
onCheckedChange = { viewModel.setATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION, if (it) byteArrayOf(0x01) else byteArrayOf(0x00)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
|
if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") {
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "transparency_customization",
|
to = "transparency_customization",
|
||||||
name = stringResource(R.string.customize_transparency_mode),
|
name = stringResource(R.string.customize_transparency_mode),
|
||||||
@@ -378,12 +242,13 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val toneVolumeValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME]?.getOrNull(0)?.toFloat() ?: 75f
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.tone_volume),
|
label = stringResource(R.string.tone_volume),
|
||||||
description = stringResource(R.string.tone_volume_description),
|
description = stringResource(R.string.tone_volume_description),
|
||||||
mutableFloatState = toneVolumeValue,
|
value = toneVolumeValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
toneVolumeValue.floatValue = it
|
viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, byteArrayOf(it.toInt().toByte(), 0x50))
|
||||||
},
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
snapPoints = listOf(75f),
|
snapPoints = listOf(75f),
|
||||||
@@ -392,11 +257,13 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
|
|
||||||
if (capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
|
if (state.capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
|
||||||
|
val volumeSwipeEnabled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE]?.getOrNull(0)?.toInt() == 0x01
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.volume_control),
|
label = stringResource(R.string.volume_control),
|
||||||
description = stringResource(R.string.volume_control_description),
|
description = stringResource(R.string.volume_control_description),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE,
|
checked = volumeSwipeEnabled,
|
||||||
|
onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
@@ -406,8 +273,8 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
selectedOption = selectedVolumeSwipeSpeed?: stringResource(R.string.default_option),
|
selectedOption = selectedVolumeSwipeSpeed?: stringResource(R.string.default_option),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedVolumeSwipeSpeed = newValue
|
selectedVolumeSwipeSpeed = newValue
|
||||||
aacpManager?.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL,
|
||||||
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
||||||
?: 1.toByte()
|
?: 1.toByte()
|
||||||
)
|
)
|
||||||
@@ -418,7 +285,7 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
|
// if (!hearingAidEnabled.value&& BuildConfig.FLAVOR == "xposed") {
|
||||||
// Text(
|
// Text(
|
||||||
// text = stringResource(R.string.apply_eq_to),
|
// text = stringResource(R.string.apply_eq_to),
|
||||||
// style = TextStyle(
|
// style = TextStyle(
|
||||||
@@ -640,7 +507,7 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-62
@@ -18,84 +18,37 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveStrengthScreen(navController: NavController) {
|
fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val sliderValue = remember { mutableFloatStateOf(0f) }
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
|
|
||||||
LaunchedEffect(sliderValue) {
|
|
||||||
val sliderValueFromAACP = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
sliderValueFromAACP?.toFloat()?.let { sliderValue.floatValue = (100 - it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val listener = remember {
|
|
||||||
object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value) {
|
|
||||||
controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)?.toFloat()?.let {
|
|
||||||
sliderValue.floatValue = (100 - it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
|
|
||||||
listener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
@@ -109,17 +62,26 @@ fun AdaptiveStrengthScreen(navController: NavController) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
val sliderValue = remember {
|
||||||
|
mutableFloatStateOf(
|
||||||
|
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH]?.getOrNull(
|
||||||
|
0
|
||||||
|
)?.toFloat() ?: 50f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var job by remember { mutableStateOf<Job?>(null) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.customize_adaptive_audio),
|
label = stringResource(R.string.customize_adaptive_audio),
|
||||||
mutableFloatState = sliderValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
sliderValue.floatValue = it
|
sliderValue.floatValue = it
|
||||||
debounceJob?.cancel()
|
job?.cancel()
|
||||||
debounceJob = CoroutineScope(Dispatchers.Default).launch {
|
job = scope.launch {
|
||||||
delay(300)
|
delay(150)
|
||||||
service.aacpManager.sendControlCommand(
|
viewModel.setControlCommandValue(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value,
|
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
|
||||||
(100 - it).toInt()
|
byteArrayOf((100 - it).toInt().toByte())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+265
-206
@@ -20,16 +20,10 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.Context.RECEIVER_EXPORTED
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -46,10 +40,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -65,23 +59,19 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import com.kyant.backdrop.drawBackdrop
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import com.kyant.backdrop.highlight.Highlight
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.launch
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.AboutCard
|
import me.kavishdevar.librepods.composables.AboutCard
|
||||||
import me.kavishdevar.librepods.composables.AudioSettings
|
import me.kavishdevar.librepods.composables.AudioSettings
|
||||||
import me.kavishdevar.librepods.composables.BatteryView
|
import me.kavishdevar.librepods.composables.BatteryView
|
||||||
import me.kavishdevar.librepods.composables.CallControlSettings
|
import me.kavishdevar.librepods.composables.CallControlSettings
|
||||||
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
|
||||||
import me.kavishdevar.librepods.composables.ConnectionSettings
|
import me.kavishdevar.librepods.composables.ConnectionSettings
|
||||||
import me.kavishdevar.librepods.composables.HearingHealthSettings
|
import me.kavishdevar.librepods.composables.HearingHealthSettings
|
||||||
import me.kavishdevar.librepods.composables.MicrophoneSettings
|
import me.kavishdevar.librepods.composables.MicrophoneSettings
|
||||||
@@ -92,39 +82,33 @@ import me.kavishdevar.librepods.composables.StyledButton
|
|||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
|
import me.kavishdevar.librepods.utils.AirPodsPro3
|
||||||
import me.kavishdevar.librepods.utils.Capability
|
import me.kavishdevar.librepods.utils.Capability
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavController) {
|
||||||
navController: NavController, isConnected: Boolean, isRemotelyConnected: Boolean) {
|
val state by viewModel.uiState.collectAsState()
|
||||||
var isLocallyConnected by remember { mutableStateOf(isConnected) }
|
|
||||||
var isRemotelyConnected by remember { mutableStateOf(isRemotelyConnected) }
|
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
var device by remember { mutableStateOf(dev) }
|
|
||||||
var deviceName by remember {
|
var deviceName by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
TextFieldValue(
|
TextFieldValue(
|
||||||
sharedPreferences.getString("name", device?.name ?: "AirPods Pro").toString()
|
sharedPreferences.getString("name", state.deviceName).toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(service) {
|
|
||||||
isLocallyConnected = service.isConnectedLocally
|
|
||||||
}
|
|
||||||
|
|
||||||
val nameChangeListener = remember {
|
val nameChangeListener = remember {
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
if (key == "name") {
|
if (key == "name") {
|
||||||
deviceName = TextFieldValue(sharedPreferences.getString("name", "AirPods Pro").toString())
|
deviceName =
|
||||||
|
TextFieldValue(sharedPreferences.getString("name", "AirPods Pro").toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,113 +121,28 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
fun handleRemoteConnection(connected: Boolean) {
|
LaunchedEffect(Unit) {
|
||||||
isRemotelyConnected = connected
|
viewModel.refreshInitialData()
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
isSystemInDarkTheme()
|
||||||
|
|
||||||
val connectionReceiver = remember {
|
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
when (intent?.action) {
|
|
||||||
"me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY" -> {
|
|
||||||
coroutineScope.launch {
|
|
||||||
handleRemoteConnection(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY" -> {
|
|
||||||
coroutineScope.launch {
|
|
||||||
handleRemoteConnection(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AirPodsNotifications.AIRPODS_CONNECTED -> {
|
|
||||||
coroutineScope.launch {
|
|
||||||
isLocallyConnected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AirPodsNotifications.AIRPODS_DISCONNECTED -> {
|
|
||||||
coroutineScope.launch {
|
|
||||||
isLocallyConnected = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AirPodsNotifications.DISCONNECT_RECEIVERS -> {
|
|
||||||
try {
|
|
||||||
context?.unregisterReceiver(this)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
val filter = IntentFilter().apply {
|
|
||||||
addAction("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
|
||||||
addAction("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
|
||||||
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
|
||||||
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
|
||||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.registerReceiver(connectionReceiver, filter, RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
context.registerReceiver(connectionReceiver, filter)
|
|
||||||
}
|
|
||||||
onDispose {
|
|
||||||
try {
|
|
||||||
context.unregisterReceiver(connectionReceiver)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(service) {
|
|
||||||
service.let {
|
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
|
||||||
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
|
||||||
})
|
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
|
||||||
putExtra("data", it.getANC())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val darkMode = isSystemInDarkTheme()
|
|
||||||
val hazeStateS = remember { mutableStateOf(HazeState()) }
|
val hazeStateS = remember { mutableStateOf(HazeState()) }
|
||||||
|
|
||||||
// val showDialog = remember { mutableStateOf(!sharedPreferences.getBoolean("donationDialogShown", false)) }
|
|
||||||
|
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = deviceName.text,
|
title = deviceName.text, actionButtons = listOf(
|
||||||
actionButtons = listOf(
|
{ scaffoldBackdrop ->
|
||||||
{scaffoldBackdrop ->
|
StyledIconButton(
|
||||||
StyledIconButton(
|
onClick = { navController.navigate("app_settings") },
|
||||||
onClick = { navController.navigate("app_settings") },
|
icon = "",
|
||||||
icon = "",
|
backdrop = scaffoldBackdrop
|
||||||
darkMode = darkMode,
|
)
|
||||||
backdrop = scaffoldBackdrop
|
}), snackbarHostState = snackbarHostState
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
snackbarHostState = snackbarHostState
|
|
||||||
) { spacerHeight, hazeState ->
|
) { spacerHeight, hazeState ->
|
||||||
hazeStateS.value = hazeState
|
hazeStateS.value = hazeState
|
||||||
if (isLocallyConnected || isRemotelyConnected) {
|
|
||||||
val instance = service.airpodsInstance
|
if (state.isLocallyConnected) {
|
||||||
if (instance == null) {
|
val capabilities = state.capabilities
|
||||||
Text("Error: AirPods instance is null")
|
|
||||||
return@StyledScaffold
|
|
||||||
}
|
|
||||||
val capabilities = instance.model.capabilities
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -252,7 +151,11 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
) {
|
) {
|
||||||
item(key = "spacer_top") { Spacer(modifier = Modifier.height(spacerHeight)) }
|
item(key = "spacer_top") { Spacer(modifier = Modifier.height(spacerHeight)) }
|
||||||
item(key = "battery") {
|
item(key = "battery") {
|
||||||
BatteryView(service = service)
|
BatteryView(
|
||||||
|
batteryList = state.battery,
|
||||||
|
budsRes = state.instance?.model?.budsRes ?: R.drawable.airpods_pro_2_case,
|
||||||
|
caseRes = state.instance?.model?.caseRes ?: R.drawable.airpods_pro_2_case
|
||||||
|
)
|
||||||
}
|
}
|
||||||
item(key = "spacer_battery") { Spacer(modifier = Modifier.height(32.dp)) }
|
item(key = "spacer_battery") { Spacer(modifier = Modifier.height(32.dp)) }
|
||||||
|
|
||||||
@@ -265,79 +168,261 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// val actAsAppleDeviceHookEnabled = RadareOffsetFinder.isSdpOffsetAvailable()
|
|
||||||
// if (actAsAppleDeviceHookEnabled) {
|
val hasHearingAidCapability =
|
||||||
// item(key = "spacer_hearing_health") { Spacer(modifier = Modifier.height(32.dp)) }
|
state.instance?.model?.capabilities?.contains(Capability.HEARING_AID) == true
|
||||||
// item(key = "hearing_health") {
|
val hasPPECapability =
|
||||||
// HearingHealthSettings(navController = navController)
|
state.instance?.model?.capabilities?.contains(Capability.PPE) == true
|
||||||
// }
|
|
||||||
// }
|
if (hasHearingAidCapability || hasPPECapability) {
|
||||||
|
if (hasPPECapability || (BuildConfig.FLAVOR == "xposed" && hasHearingAidCapability)) item(
|
||||||
|
key = "spacer_hearing_health"
|
||||||
|
) { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
item(key = "hearing_health") {
|
||||||
|
HearingHealthSettings(
|
||||||
|
navController = navController,
|
||||||
|
hasPPECapability = hasPPECapability,
|
||||||
|
hasHearingAidCapability = hasHearingAidCapability,
|
||||||
|
isXposed = BuildConfig.FLAVOR == "xposed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (capabilities.contains(Capability.LISTENING_MODE)) {
|
if (capabilities.contains(Capability.LISTENING_MODE)) {
|
||||||
item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "noise_control") { NoiseControlSettings(service = service) }
|
item(key = "noise_control") {
|
||||||
|
NoiseControlSettings(
|
||||||
|
showOffListeningMode = state.offListeningMode,
|
||||||
|
noiseControlModeValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE]?.getOrNull(
|
||||||
|
0
|
||||||
|
)?.toInt() ?: 3,
|
||||||
|
onNoiseControlModeChanged = {
|
||||||
|
viewModel.setControlCommandInt(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities.contains(Capability.STEM_CONFIG)) {
|
if (capabilities.contains(Capability.STEM_CONFIG)) {
|
||||||
item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "press_hold") { PressAndHoldSettings(navController = navController) }
|
item(key = "press_hold") {
|
||||||
|
PressAndHoldSettings(
|
||||||
|
navController = navController,
|
||||||
|
leftAction = state.leftAction,
|
||||||
|
rightAction = state.rightAction
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = "spacer_call") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_call") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "call_control") { CallControlSettings(hazeState = hazeState) }
|
item(key = "call_control") {
|
||||||
|
val flipped =
|
||||||
|
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG]?.take(
|
||||||
|
2
|
||||||
|
)?.equals(byteArrayOf(0x00.toByte(), 0x02.toByte()))
|
||||||
|
CallControlSettings(
|
||||||
|
hazeState = hazeState,
|
||||||
|
flipped = flipped == true,
|
||||||
|
onCallControlValueChanged = {
|
||||||
|
viewModel.setControlCommandValue(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG,
|
||||||
|
if (it) byteArrayOf(0x00, 0x02) else byteArrayOf(0x00, 0x03)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (capabilities.contains(Capability.STEM_CONFIG)) {
|
if (capabilities.contains(Capability.STEM_CONFIG) && !BuildConfig.PLAY_BUILD) {
|
||||||
item(key = "spacer_camera") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_camera") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "camera_control") { NavigationButton(to = "camera_control", name = stringResource(R.string.camera_remote), description = stringResource(R.string.camera_control_description), title = stringResource(R.string.camera_control), navController = navController) }
|
item(key = "camera_control") {
|
||||||
|
NavigationButton(
|
||||||
|
to = "camera_control",
|
||||||
|
name = stringResource(R.string.camera_remote),
|
||||||
|
description = stringResource(R.string.camera_control_description),
|
||||||
|
title = stringResource(R.string.camera_control),
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "upgrade_button") {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||||
|
|
||||||
|
if (!state.isPremium) {
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.purchase(context)
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
tint = Color(0xFF916100)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.unlock_all_features),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "audio") { AudioSettings(navController = navController) }
|
item(key = "audio") {
|
||||||
|
val model = state.instance?.model ?: AirPodsPro3()
|
||||||
|
val adaptiveVolumeCapability =
|
||||||
|
model.capabilities.contains(Capability.ADAPTIVE_VOLUME)
|
||||||
|
val conversationalAwarenessCapability =
|
||||||
|
model.capabilities.contains(Capability.CONVERSATION_AWARENESS)
|
||||||
|
val loudSoundReductionCapability =
|
||||||
|
model.capabilities.contains(Capability.LOUD_SOUND_REDUCTION)
|
||||||
|
val adaptiveAudioCapability =
|
||||||
|
model.capabilities.contains(Capability.ADAPTIVE_VOLUME)
|
||||||
|
|
||||||
|
val adaptiveVolumeChecked =
|
||||||
|
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(
|
||||||
|
0
|
||||||
|
) == 0x01.toByte()
|
||||||
|
val conversationalAwarenessChecked =
|
||||||
|
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG]?.getOrNull(
|
||||||
|
0
|
||||||
|
) == 0x01.toByte()
|
||||||
|
val loudSoundReduction =
|
||||||
|
viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)
|
||||||
|
?.getOrNull(0) == 0x01.toByte()
|
||||||
|
|
||||||
|
val isXposed = BuildConfig.FLAVOR == "xposed"
|
||||||
|
AudioSettings(
|
||||||
|
navController = navController,
|
||||||
|
adaptiveVolumeCapability = adaptiveVolumeCapability,
|
||||||
|
conversationalAwarenessCapability = conversationalAwarenessCapability,
|
||||||
|
loudSoundReductionCapability = loudSoundReductionCapability,
|
||||||
|
adaptiveAudioCapability = adaptiveAudioCapability,
|
||||||
|
adaptiveVolumeChecked = adaptiveVolumeChecked,
|
||||||
|
onAdaptiveVolumeCheckedChange = { checked ->
|
||||||
|
viewModel.setControlCommandBoolean(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
||||||
|
checked
|
||||||
|
)
|
||||||
|
},
|
||||||
|
conversationalAwarenessChecked = conversationalAwarenessChecked && state.isPremium,
|
||||||
|
onConversationalAwarenessCheckedChange = { checked ->
|
||||||
|
viewModel.setControlCommandBoolean(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
||||||
|
checked
|
||||||
|
)
|
||||||
|
},
|
||||||
|
loudSoundReductionChecked = loudSoundReduction,
|
||||||
|
onLoudSoundReductionCheckedChange = {
|
||||||
|
viewModel.setATTCharacteristicValue(
|
||||||
|
ATTHandles.LOUD_SOUND_REDUCTION,
|
||||||
|
byteArrayOf(if (it) 0x01.toByte() else 0x00.toByte())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isXposed = isXposed,
|
||||||
|
isPremium = state.isPremium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "connection") { ConnectionSettings() }
|
item(key = "connection") {
|
||||||
|
ConnectionSettings(
|
||||||
|
automaticEarDetectionEnabled = state.automaticEarDetectionEnabled,
|
||||||
|
onAutomaticEarDetectionChanged = {
|
||||||
|
viewModel.setAutomaticEarDetectionEnabled(it)
|
||||||
|
},
|
||||||
|
automaticConnectionEnabled = state.automaticConnectionEnabled,
|
||||||
|
onAutomaticConnectionChanged = { viewModel.setAutomaticConnectionEnabled(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
item(key = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "microphone") { MicrophoneSettings(hazeState) }
|
item(key = "microphone") {
|
||||||
|
val id = AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE
|
||||||
|
MicrophoneSettings(
|
||||||
|
hazeState = hazeState,
|
||||||
|
micModeValue = state.controlStates[id]?.getOrNull(0) ?: 0x00.toByte(),
|
||||||
|
onMicModeValueChanged = { viewModel.setControlCommandByte(id, it) })
|
||||||
|
}
|
||||||
|
|
||||||
if (capabilities.contains(Capability.SLEEP_DETECTION)) {
|
if (capabilities.contains(Capability.SLEEP_DETECTION)) {
|
||||||
item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_sleep") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "sleep_detection") {
|
item(key = "sleep_detection") {
|
||||||
|
val id =
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.sleep_detection),
|
label = stringResource(R.string.sleep_detection),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
checked = state.controlStates[id]?.getOrNull(0) == 0x01.toByte(),
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.setControlCommandBoolean(id, it)
|
||||||
|
},
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities.contains(Capability.HEAD_GESTURES)) {
|
if (capabilities.contains(Capability.HEAD_GESTURES)) {
|
||||||
item(key = "spacer_head_tracking") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_head_tracking") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "head_tracking") { NavigationButton(to = "head_tracking", name = stringResource(R.string.head_gestures), navController = navController, currentState = if (sharedPreferences.getBoolean("head_gestures", false)) stringResource(R.string.on) else stringResource(R.string.off)) }
|
item(key = "head_tracking") {
|
||||||
|
NavigationButton(
|
||||||
|
to = "head_tracking",
|
||||||
|
name = stringResource(R.string.head_gestures),
|
||||||
|
navController = navController,
|
||||||
|
currentState = if (sharedPreferences.getBoolean(
|
||||||
|
"head_gestures", false
|
||||||
|
)
|
||||||
|
) stringResource(R.string.on) else stringResource(R.string.off)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "accessibility") { NavigationButton(to = "accessibility", name = stringResource(R.string.accessibility), navController = navController) }
|
item(key = "accessibility") {
|
||||||
|
NavigationButton(
|
||||||
|
to = "accessibility",
|
||||||
|
name = stringResource(R.string.accessibility),
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)){
|
if (capabilities.contains(Capability.LOUD_SOUND_REDUCTION)) {
|
||||||
item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "off_listening") {
|
item(key = "off_listening") {
|
||||||
|
val id = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.off_listening_mode),
|
label = stringResource(R.string.off_listening_mode),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
description = stringResource(R.string.off_listening_mode_description),
|
||||||
description = stringResource(R.string.off_listening_mode_description)
|
checked = state.controlStates[id]?.getOrNull(0) == 0x01.toByte(),
|
||||||
|
onCheckedChange = viewModel::setOffListeningMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = "spacer_about") { Spacer(modifier = Modifier.height(32.dp)) }
|
item(key = "spacer_about") { Spacer(modifier = Modifier.height(32.dp)) }
|
||||||
item(key = "about") { AboutCard(navController = navController) }
|
item(key = "about") {
|
||||||
|
AboutCard(
|
||||||
|
navController = navController,
|
||||||
|
modelName = state.modelName,
|
||||||
|
actualModel = state.actualModel,
|
||||||
|
serialNumbers = state.serialNumbers,
|
||||||
|
version = state.version3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
// item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "debug") { NavigationButton("debug", "Debug", navController) }
|
// item(key = "debug") { NavigationButton("debug", "Debug", navController) }
|
||||||
item(key = "spacer_bottom") { Spacer(Modifier.height(24.dp)) }
|
item(key = "spacer_bottom") { Spacer(Modifier.height(24.dp)) }
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -348,23 +433,20 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
shape = { RoundedCornerShape(0.dp) },
|
shape = { RoundedCornerShape(0.dp) },
|
||||||
highlight = {
|
highlight = {
|
||||||
Highlight.Ambient.copy(alpha = 0f)
|
Highlight.Ambient.copy(alpha = 0f)
|
||||||
}
|
},
|
||||||
)
|
effects = {})
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.airpods_not_connected),
|
text = stringResource(R.string.airpods_not_connected), style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -379,34 +461,30 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
StyledButton(
|
// StyledButton(
|
||||||
onClick = { navController.navigate("troubleshooting") },
|
// onClick = { navController.navigate("troubleshooting") },
|
||||||
backdrop = backdrop,
|
// backdrop = backdrop,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.fillMaxWidth(0.9f)
|
// .fillMaxWidth(0.9f)
|
||||||
) {
|
// ) {
|
||||||
Text(
|
// Text(
|
||||||
text = stringResource(R.string.troubleshooting),
|
// text = stringResource(R.string.troubleshooting),
|
||||||
style = TextStyle(
|
// style = TextStyle(
|
||||||
fontSize = 16.sp,
|
// fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
// fontWeight = FontWeight.Medium,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
// color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
Spacer(Modifier.height(16.dp))
|
// Spacer(Modifier.height(16.dp))
|
||||||
StyledButton(
|
StyledButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
service.reconnectFromSavedMac()
|
viewModel.reconnectFromSavedMac()
|
||||||
},
|
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
|
||||||
backdrop = backdrop,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(0.9f)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.reconnect_to_last_device),
|
text = stringResource(R.string.reconnect_to_last_device), style = TextStyle(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -417,37 +495,18 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConfirmationDialog(
|
|
||||||
showDialog = showDialog,
|
|
||||||
title = stringResource(R.string.support_librepods),
|
|
||||||
message = stringResource(R.string.support_dialog_description),
|
|
||||||
confirmText = stringResource(R.string.support_me) + " \uDBC0\uDEB5",
|
|
||||||
dismissText = stringResource(R.string.never_show_again),
|
|
||||||
onConfirm = {
|
|
||||||
val browserIntent = Intent(
|
|
||||||
Intent.ACTION_VIEW,
|
|
||||||
"https://github.com/sponsors/kavishdevar".toUri()
|
|
||||||
)
|
|
||||||
context.startActivity(browserIntent)
|
|
||||||
sharedPreferences.edit { putBoolean("donationDialogShown", true) }
|
|
||||||
},
|
|
||||||
onDismiss = {
|
|
||||||
sharedPreferences.edit { putBoolean("donationDialogShown", true) }
|
|
||||||
},
|
|
||||||
hazeState = hazeStateS.value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreenPreview() {
|
fun AirPodsSettingsScreenPreview() {
|
||||||
Column (
|
Column(
|
||||||
modifier = Modifier.height(2000.dp)
|
modifier = Modifier.height(2000.dp)
|
||||||
) {
|
) {
|
||||||
LibrePodsTheme (
|
LibrePodsTheme(
|
||||||
darkTheme = true
|
darkTheme = true
|
||||||
) {
|
) {
|
||||||
AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true, isRemotelyConnected = false)
|
// AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true, isRemotelyConnected = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,114 +18,79 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.SelectItem
|
import me.kavishdevar.librepods.composables.SelectItem
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSelectList
|
import me.kavishdevar.librepods.composables.StyledSelectList
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.services.AppListenerService
|
import me.kavishdevar.librepods.services.AppListenerService
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
|
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CameraControlScreen(navController: NavController) {
|
fun CameraControlScreen(viewModel: AirPodsViewModel) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val currentCameraAction by viewModel.cameraAction.collectAsState()
|
||||||
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
var currentCameraAction by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
sharedPreferences.getString("camera_action", null)?.let { StemPressType.valueOf(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAppListenerServiceEnabled(context: Context): Boolean {
|
fun isAppListenerServiceEnabled(context: Context): Boolean {
|
||||||
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||||
val enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
|
val enabledServices =
|
||||||
|
am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
|
||||||
val serviceComponent = ComponentName(context, AppListenerService::class.java)
|
val serviceComponent = ComponentName(context, AppListenerService::class.java)
|
||||||
return enabledServices.any { it.resolveInfo.serviceInfo.packageName == serviceComponent.packageName && it.resolveInfo.serviceInfo.name == serviceComponent.className }
|
return enabledServices.any {
|
||||||
|
it.resolveInfo.serviceInfo.packageName == serviceComponent.packageName &&
|
||||||
|
it.resolveInfo.serviceInfo.name == serviceComponent.className
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val cameraOptions = listOf(
|
fun handleSelection(action: StemPressType?) {
|
||||||
SelectItem(
|
if (action != null && !isAppListenerServiceEnabled(context)) {
|
||||||
name = stringResource(R.string.off),
|
context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||||
selected = currentCameraAction == null,
|
} else {
|
||||||
onClick = {
|
viewModel.setCameraAction(action)
|
||||||
sharedPreferences.edit { remove("camera_action") }
|
}
|
||||||
currentCameraAction = null
|
}
|
||||||
}
|
|
||||||
),
|
val cameraOptions = remember(currentCameraAction) {
|
||||||
SelectItem(
|
listOf(
|
||||||
name = stringResource(R.string.press_once),
|
SelectItem(
|
||||||
selected = currentCameraAction == StemPressType.SINGLE_PRESS,
|
name = "Off",
|
||||||
onClick = {
|
selected = currentCameraAction == null,
|
||||||
if (!isAppListenerServiceEnabled(context)) {
|
onClick = { handleSelection(null) }
|
||||||
context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
),
|
||||||
} else {
|
SelectItem(
|
||||||
sharedPreferences.edit { putString("camera_action", StemPressType.SINGLE_PRESS.name) }
|
name = "Press once",
|
||||||
currentCameraAction = StemPressType.SINGLE_PRESS
|
selected = currentCameraAction == StemPressType.SINGLE_PRESS,
|
||||||
}
|
onClick = { handleSelection(StemPressType.SINGLE_PRESS) }
|
||||||
}
|
),
|
||||||
),
|
SelectItem(
|
||||||
SelectItem(
|
name = "Press and hold AirPods",
|
||||||
name = stringResource(R.string.press_and_hold_airpods),
|
selected = currentCameraAction == StemPressType.LONG_PRESS,
|
||||||
selected = currentCameraAction == StemPressType.LONG_PRESS,
|
onClick = { handleSelection(StemPressType.LONG_PRESS) }
|
||||||
onClick = {
|
)
|
||||||
if (!isAppListenerServiceEnabled(context)) {
|
|
||||||
context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
|
||||||
} else {
|
|
||||||
sharedPreferences.edit { putString("camera_action", StemPressType.LONG_PRESS.name) }
|
|
||||||
currentCameraAction = StemPressType.LONG_PRESS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
|
|||||||
@@ -335,7 +335,6 @@ fun DebugScreen(navController: NavController) {
|
|||||||
expandedItems.value = emptySet()
|
expandedItems.value = emptySet()
|
||||||
},
|
},
|
||||||
icon = "",
|
icon = "",
|
||||||
darkMode = isDarkTheme,
|
|
||||||
backdrop = scaffoldBackdrop
|
backdrop = scaffoldBackdrop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -83,7 +80,6 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
@@ -100,6 +96,7 @@ import me.kavishdevar.librepods.composables.StyledScaffold
|
|||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.HeadTracking
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
@@ -107,14 +104,14 @@ import kotlin.math.sin
|
|||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HeadTrackingScreen() {
|
fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
ServiceManager.getService()?.startHeadTracking()
|
viewModel.startHeadTracking()
|
||||||
onDispose {
|
onDispose {
|
||||||
ServiceManager.getService()?.stopHeadTracking()
|
viewModel.stopHeadTracking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
@@ -127,25 +124,22 @@ fun HeadTrackingScreen() {
|
|||||||
title = stringResource(R.string.head_tracking),
|
title = stringResource(R.string.head_tracking),
|
||||||
actionButtons = listOf(
|
actionButtons = listOf(
|
||||||
{ scaffoldBackdrop ->
|
{ scaffoldBackdrop ->
|
||||||
var isActive by remember { mutableStateOf(ServiceManager.getService()?.isHeadTrackingActive == true) }
|
|
||||||
StyledIconButton(
|
StyledIconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (ServiceManager.getService()?.isHeadTrackingActive == false) {
|
if (!state.headTrackingActive) {
|
||||||
ServiceManager.getService()?.startHeadTracking()
|
viewModel.startHeadTracking()
|
||||||
Log.d("HeadTrackingScreen", "Head tracking started")
|
Log.d("HeadTrackingScreen", "Head tracking started")
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.getService()?.stopHeadTracking()
|
viewModel.stopHeadTracking()
|
||||||
Log.d("HeadTrackingScreen", "Head tracking stopped")
|
Log.d("HeadTrackingScreen", "Head tracking stopped")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = if (isActive) "" else "",
|
icon = if (state.headTrackingActive) "" else "",
|
||||||
darkMode = isDarkTheme,
|
|
||||||
backdrop = scaffoldBackdrop
|
backdrop = scaffoldBackdrop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
) { spacerHeight, hazeState ->
|
) { spacerHeight, hazeState ->
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
var gestureText by remember { mutableStateOf("") }
|
var gestureText by remember { mutableStateOf("") }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -167,10 +161,37 @@ fun HeadTrackingScreen() {
|
|||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
if (!state.isPremium) {
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.purchase(context)
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
tint = Color(0xFF916100)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.unlock_all_features),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = "Head Gestures",
|
label = "Head Gestures",
|
||||||
sharedPreferences = sharedPreferences,
|
checked = state.headGesturesEnabled,
|
||||||
sharedPreferenceKey = "head_gestures",
|
onCheckedChange = { viewModel.setHeadGesturesEnabled(it) },
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
@@ -739,11 +760,3 @@ private fun AccelerationPlot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun HeadTrackingScreenPreview() {
|
|
||||||
HeadTrackingScreen()
|
|
||||||
}
|
|
||||||
|
|||||||
+14
-42
@@ -31,9 +31,10 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -41,7 +42,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
@@ -59,6 +59,7 @@ import me.kavishdevar.librepods.utils.ATTHandles
|
|||||||
import me.kavishdevar.librepods.utils.HearingAidSettings
|
import me.kavishdevar.librepods.utils.HearingAidSettings
|
||||||
import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
|
import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.sendHearingAidSettings
|
import me.kavishdevar.librepods.utils.sendHearingAidSettings
|
||||||
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -69,13 +70,14 @@ private const val TAG = "HearingAidAdjustments"
|
|||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController) {
|
fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) {
|
||||||
isSystemInDarkTheme()
|
isSystemInDarkTheme()
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
||||||
|
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.adjustments)
|
title = stringResource(R.string.adjustments)
|
||||||
@@ -125,25 +127,6 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val hearingAidEnabled = remember {
|
|
||||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
|
||||||
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
|
|
||||||
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
val hearingAidListener = remember {
|
|
||||||
object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
|
|
||||||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
|
|
||||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
|
||||||
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
|
|
||||||
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hearingAidATTListener = remember {
|
val hearingAidATTListener = remember {
|
||||||
object : (ByteArray) -> Unit {
|
object : (ByteArray) -> Unit {
|
||||||
override fun invoke(value: ByteArray) {
|
override fun invoke(value: ByteArray) {
|
||||||
@@ -165,19 +148,6 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
|
||||||
attManager.unregisterListener(ATTHandles.HEARING_AID, hearingAidATTListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, ownVoiceAmplification.floatValue, initialLoadComplete.value, initialReadSucceeded.value) {
|
LaunchedEffect(amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, ownVoiceAmplification.floatValue, initialLoadComplete.value, initialReadSucceeded.value) {
|
||||||
if (!initialLoadComplete.value) {
|
if (!initialLoadComplete.value) {
|
||||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||||
@@ -256,7 +226,7 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.amplification),
|
label = stringResource(R.string.amplification),
|
||||||
valueRange = -1f..1f,
|
valueRange = -1f..1f,
|
||||||
mutableFloatState = amplificationSliderValue,
|
value = amplificationSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
amplificationSliderValue.floatValue = it
|
amplificationSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -268,14 +238,15 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.swipe_to_control_amplification),
|
label = stringResource(R.string.swipe_to_control_amplification),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE,
|
checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE]?.getOrNull(0) == 0x01.toByte(),
|
||||||
|
onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE, it) },
|
||||||
description = stringResource(R.string.swipe_amplification_description)
|
description = stringResource(R.string.swipe_amplification_description)
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.balance),
|
label = stringResource(R.string.balance),
|
||||||
valueRange = -1f..1f,
|
valueRange = -1f..1f,
|
||||||
mutableFloatState = balanceSliderValue,
|
value = balanceSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
balanceSliderValue.floatValue = it
|
balanceSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -288,7 +259,7 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.tone),
|
label = stringResource(R.string.tone),
|
||||||
valueRange = -1f..1f,
|
valueRange = -1f..1f,
|
||||||
mutableFloatState = toneSliderValue,
|
value = toneSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
toneSliderValue.floatValue = it
|
toneSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -300,7 +271,7 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.ambient_noise_reduction),
|
label = stringResource(R.string.ambient_noise_reduction),
|
||||||
valueRange = 0f..1f,
|
valueRange = 0f..1f,
|
||||||
mutableFloatState = ambientNoiseReductionSliderValue,
|
value = ambientNoiseReductionSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
ambientNoiseReductionSliderValue.floatValue = it
|
ambientNoiseReductionSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -311,7 +282,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.conversation_boost),
|
label = stringResource(R.string.conversation_boost),
|
||||||
checkedState = conversationBoostEnabled,
|
checked = conversationBoostEnabled.value,
|
||||||
|
onCheckedChange = { conversationBoostEnabled.value = it },
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.conversation_boost_description)
|
description = stringResource(R.string.conversation_boost_description)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ import androidx.compose.material3.HorizontalDivider
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -65,11 +66,11 @@ import me.kavishdevar.librepods.composables.ConfirmationDialog
|
|||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.composables.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
||||||
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
private const val TAG = "AccessibilitySettings"
|
private const val TAG = "AccessibilitySettings"
|
||||||
@@ -78,23 +79,22 @@ private const val TAG = "AccessibilitySettings"
|
|||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingAidScreen(navController: NavController) {
|
fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: return
|
|
||||||
|
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
|
||||||
|
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
val showDialog = remember { mutableStateOf(false) }
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
val initialLoad = remember { mutableStateOf(true) }
|
val initialLoad = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val hearingAidEnabled = remember {
|
val hearingAidEnabled = remember {
|
||||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
val aidStatus = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]
|
||||||
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
|
val assistStatus = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG]
|
||||||
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
|
mutableStateOf((aidStatus?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.getOrNull(0) == 0x01.toByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
val hazeStateS = remember { mutableStateOf(HazeState()) } // dont question this. i could possibly use something other than initializing it with an empty state and then replacing it with the the one provided by the scaffold
|
val hazeStateS = remember { mutableStateOf(HazeState()) } // dont question this. i could possibly use something other than initializing it with an empty state and then replacing it with the the one provided by the scaffold
|
||||||
@@ -115,41 +115,16 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
hazeStateS.value = hazeState
|
hazeStateS.value = hazeState
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
val hearingAidListener = remember {
|
|
||||||
object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
|
|
||||||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
|
|
||||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
|
||||||
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
|
|
||||||
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// val mediaAssistEnabled = remember { mutableStateOf(false) }
|
// val mediaAssistEnabled = remember { mutableStateOf(false) }
|
||||||
// val adjustMediaEnabled = remember { mutableStateOf(false) }
|
// val adjustMediaEnabled = remember { mutableStateOf(false) }
|
||||||
// val adjustPhoneEnabled = remember { mutableStateOf(false) }
|
// val adjustPhoneEnabled = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(hearingAidEnabled.value) {
|
LaunchedEffect(hearingAidEnabled.value) {
|
||||||
if (hearingAidEnabled.value && !initialLoad.value) {
|
if (hearingAidEnabled.value && !initialLoad.value) {
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
} else if (!hearingAidEnabled.value && !initialLoad.value) {
|
} else if (!hearingAidEnabled.value && !initialLoad.value) {
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
|
viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x02))
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
|
viewModel.setControlCommandByte(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 0x02.toByte())
|
||||||
hearingAidEnabled.value = false
|
hearingAidEnabled.value = false
|
||||||
}
|
}
|
||||||
initialLoad.value = false
|
initialLoad.value = false
|
||||||
@@ -186,7 +161,8 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
) {
|
) {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.hearing_aid),
|
label = stringResource(R.string.hearing_aid),
|
||||||
checkedState = hearingAidEnabled,
|
checked = hearingAidEnabled.value,
|
||||||
|
onCheckedChange = { hearingAidEnabled.value = it },
|
||||||
independent = false
|
independent = false
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -269,20 +245,24 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
dismissText = "Cancel",
|
dismissText = "Cancel",
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
val enrolled = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(0) == 0x01.toByte()
|
val enrolled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0) == 0x01.toByte()
|
||||||
if (!enrolled) {
|
if (!enrolled) {
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
|
viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x01))
|
||||||
} else {
|
} else {
|
||||||
aacpManager.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
|
viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, byteArrayOf(0x01, 0x01))
|
||||||
}
|
}
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte())
|
viewModel.setControlCommandByte(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 0x01.toByte())
|
||||||
hearingAidEnabled.value = true
|
hearingAidEnabled.value = true
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
val data = viewModel.getATTCharacteristicValue(ATTHandles.TRANSPARENCY) ?: byteArrayOf()
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
Log.w(TAG, "read failed")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
val parsed = parseTransparencySettingsResponse(data)
|
val parsed = parseTransparencySettingsResponse(data)
|
||||||
val disabledSettings = parsed.copy(enabled = false)
|
val disabledSettings = parsed.copy(enabled = false)
|
||||||
sendTransparencySettings(attManager, disabledSettings)
|
sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error disabling transparency: ${e.message}")
|
Log.e(TAG, "Error disabling transparency: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-32
@@ -18,48 +18,31 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingProtectionScreen(navController: NavController) {
|
fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val service = ServiceManager.getService()
|
|
||||||
if (service == null) return
|
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.hearing_protection),
|
title = stringResource(R.string.hearing_protection),
|
||||||
) { spacerHeight ->
|
) { spacerHeight ->
|
||||||
@@ -71,20 +54,36 @@ fun HearingProtectionScreen(navController: NavController) {
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
StyledToggle(
|
if (BuildConfig.FLAVOR == "xposed") {
|
||||||
title = stringResource(R.string.environmental_noise),
|
StyledToggle(
|
||||||
label = stringResource(R.string.loud_sound_reduction),
|
title = stringResource(R.string.environmental_noise),
|
||||||
description = stringResource(R.string.loud_sound_reduction_description),
|
label = stringResource(R.string.loud_sound_reduction),
|
||||||
attHandle = ATTHandles.LOUD_SOUND_REDUCTION
|
description = stringResource(R.string.loud_sound_reduction_description),
|
||||||
)
|
checked = viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)
|
||||||
|
?.get(0)?.toInt() == 1,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.setATTCharacteristicValue(
|
||||||
|
ATTHandles.LOUD_SOUND_REDUCTION,
|
||||||
|
byteArrayOf(if (it) 1.toByte() else 0.toByte())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// attHandle = ATTHandles.LOUD_SOUND_REDUCTION
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
title = stringResource(R.string.workspace_use),
|
title = stringResource(R.string.workspace_use),
|
||||||
label = stringResource(R.string.ppe),
|
label = stringResource(R.string.ppe),
|
||||||
description = stringResource(R.string.workspace_use_description),
|
description = stringResource(R.string.workspace_use_description),
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG
|
checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG]?.getOrNull(
|
||||||
)
|
0
|
||||||
|
)?.toInt() == 1,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.setControlCommandBoolean(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, it
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+55
-65
@@ -22,37 +22,25 @@ package me.kavishdevar.librepods.screens
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -61,59 +49,38 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.SelectItem
|
import me.kavishdevar.librepods.composables.SelectItem
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.composables.StyledButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSelectList
|
import me.kavishdevar.librepods.composables.StyledSelectList
|
||||||
import me.kavishdevar.librepods.constants.StemAction
|
import me.kavishdevar.librepods.constants.StemAction
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RightDivider() {
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 72.dp, end = 20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RightDividerNoIcon() {
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = Color(0x40888888),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 20.dp, end = 20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPress(navController: NavController, name: String) {
|
fun LongPress(viewModel: AirPodsViewModel, name: String) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
val modesByte = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
val state by viewModel.uiState.collectAsState()
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
val modesByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0) ?: 0
|
||||||
|
|
||||||
|
Log.d("PressAndHoldSettingsScreen", "Current modes state: ${modesByte.toString(2)}")
|
||||||
|
Log.d("PressAndHoldSettingsScreen", "Off mode: ${(modesByte and 0x01) != 0.toByte()}")
|
||||||
|
Log.d("PressAndHoldSettingsScreen", "Transparency mode: ${(modesByte and 0x04) != 0.toByte()}")
|
||||||
|
Log.d("PressAndHoldSettingsScreen", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
|
||||||
|
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
|
||||||
|
|
||||||
if (modesByte != null) {
|
|
||||||
Log.d("PressAndHoldSettingsScreen", "Current modes state: ${modesByte.toString(2)}")
|
|
||||||
Log.d("PressAndHoldSettingsScreen", "Off mode: ${(modesByte and 0x01) != 0.toByte()}")
|
|
||||||
Log.d("PressAndHoldSettingsScreen", "Transparency mode: ${(modesByte and 0x04) != 0.toByte()}")
|
|
||||||
Log.d("PressAndHoldSettingsScreen", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
|
|
||||||
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
|
|
||||||
}
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val prefKey = if (name.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
val prefKey = if (name.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
||||||
@@ -124,9 +91,8 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = name
|
title = name
|
||||||
) { spacerHeight ->
|
) { spacerHeight ->
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layerBackdrop(backdrop)
|
.layerBackdrop(backdrop)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
@@ -148,11 +114,36 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
longPressAction = StemAction.DIGITAL_ASSISTANT
|
longPressAction = StemAction.DIGITAL_ASSISTANT
|
||||||
sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name) }
|
sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name) }
|
||||||
}
|
},
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
StyledSelectList(items = actionItems)
|
StyledSelectList(items = actionItems)
|
||||||
|
|
||||||
|
if (!state.isPremium) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.purchase(context)
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
tint = Color(0xFF916100)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.unlock_all_features),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
if (longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES) {
|
if (longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES) {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -176,10 +167,11 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
val allowOff = offListeningModeValue == 1.toByte()
|
val allowOff = offListeningModeValue == 1.toByte()
|
||||||
Log.d("PressAndHoldSettingsScreen", "Allow Off option: $allowOff")
|
Log.d("PressAndHoldSettingsScreen", "Allow Off option: $allowOff")
|
||||||
|
|
||||||
val initialByte = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
val initialByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS
|
?.get(0)?.toInt()
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)?.toInt() ?: sharedPreferences.getInt("long_press_byte", 0b0101)
|
?: sharedPreferences.getInt("long_press_byte", 0b0101)
|
||||||
var currentByte by remember { mutableStateOf(initialByte) }
|
|
||||||
|
var currentByte by remember { mutableIntStateOf(initialByte) }
|
||||||
|
|
||||||
val listeningModeItems = mutableListOf<SelectItem>()
|
val listeningModeItems = mutableListOf<SelectItem>()
|
||||||
if (allowOff) {
|
if (allowOff) {
|
||||||
@@ -197,8 +189,8 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
} else {
|
} else {
|
||||||
currentByte or bit
|
currentByte or bit
|
||||||
}
|
}
|
||||||
ServiceManager.getService()!!.aacpManager.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS.value,
|
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||||
newValue.toByte()
|
newValue.toByte()
|
||||||
)
|
)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
@@ -223,8 +215,8 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
} else {
|
} else {
|
||||||
currentByte or bit
|
currentByte or bit
|
||||||
}
|
}
|
||||||
ServiceManager.getService()!!.aacpManager.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS.value,
|
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||||
newValue.toByte()
|
newValue.toByte()
|
||||||
)
|
)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
@@ -246,8 +238,8 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
} else {
|
} else {
|
||||||
currentByte or bit
|
currentByte or bit
|
||||||
}
|
}
|
||||||
ServiceManager.getService()!!.aacpManager.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS.value,
|
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||||
newValue.toByte()
|
newValue.toByte()
|
||||||
)
|
)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
@@ -269,8 +261,8 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
} else {
|
} else {
|
||||||
currentByte or bit
|
currentByte or bit
|
||||||
}
|
}
|
||||||
ServiceManager.getService()!!.aacpManager.sendControlCommand(
|
viewModel.setControlCommandByte(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS.value,
|
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||||
newValue.toByte()
|
newValue.toByte()
|
||||||
)
|
)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
@@ -296,9 +288,7 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d("PressAndHoldSettingsScreen", "Current byte: ${ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
Log.d("PressAndHoldSettingsScreen", "Current byte: ${modesByte.toString(2)}")
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)?.toString(2)}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun countEnabledModes(byteValue: Int): Int {
|
fun countEnabledModes(byteValue: Int): Int {
|
||||||
|
|||||||
@@ -53,26 +53,22 @@ import androidx.compose.ui.text.TextStyle
|
|||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RenameScreen(navController: NavController) {
|
fun RenameScreen(viewModel: AirPodsViewModel) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val name = remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", "") ?: "")) }
|
val name = remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", "") ?: "")) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -115,7 +111,7 @@ fun RenameScreen(navController: NavController) {
|
|||||||
onValueChange = {
|
onValueChange = {
|
||||||
name.value = it
|
name.value = it
|
||||||
sharedPreferences.edit {putString("name", it.text)}
|
sharedPreferences.edit {putString("name", it.text)}
|
||||||
ServiceManager.getService()?.setName(it.text)
|
viewModel.setName(it.text)
|
||||||
},
|
},
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
@@ -159,9 +155,3 @@ fun RenameScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun RenameScreenPreview() {
|
|
||||||
RenameScreen(navController = NavController(LocalContext.current))
|
|
||||||
}
|
|
||||||
|
|||||||
+29
-31
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -43,6 +44,8 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -58,23 +61,22 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
|
||||||
import me.kavishdevar.librepods.utils.TransparencySettings
|
import me.kavishdevar.librepods.utils.TransparencySettings
|
||||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
||||||
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -84,14 +86,12 @@ private const val TAG = "TransparencySettings"
|
|||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TransparencySettingsScreen(navController: NavController) {
|
fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
|
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: return
|
val attManager = ServiceManager.getService()?.attManager ?: return
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
|
||||||
val isSdpOffsetAvailable = remember { mutableStateOf(false) } // always available rn, for testing without radare
|
|
||||||
// remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
@@ -99,6 +99,9 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
|
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.customize_transparency_mode)
|
title = stringResource(R.string.customize_transparency_mode)
|
||||||
){ spacerHeight, hazeState ->
|
){ spacerHeight, hazeState ->
|
||||||
@@ -205,7 +208,7 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
balance = balanceSliderValue.floatValue
|
balance = balanceSliderValue.floatValue
|
||||||
)
|
)
|
||||||
Log.d("TransparencySettings", "Updated settings: ${transparencySettings.value}")
|
Log.d("TransparencySettings", "Updated settings: ${transparencySettings.value}")
|
||||||
sendTransparencySettings(attManager, transparencySettings.value)
|
sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
@@ -222,18 +225,14 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
||||||
try {
|
try {
|
||||||
if (aacpManager != null) {
|
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
||||||
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
val aacpEQ = state.eqData
|
||||||
val aacpEQ = aacpManager.eqData
|
if (aacpEQ.isNotEmpty()) {
|
||||||
if (aacpEQ.isNotEmpty()) {
|
eq.value = aacpEQ.copyOf()
|
||||||
eq.value = aacpEQ.copyOf()
|
phoneMediaEQ.value = aacpEQ.copyOf()
|
||||||
phoneMediaEQ.value = aacpEQ.copyOf()
|
Log.d(TAG, "Populated EQ from AACPManager: ${aacpEQ.toList()}")
|
||||||
Log.d(TAG, "Populated EQ from AACPManager: ${aacpEQ.toList()}")
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "AACPManager EQ data empty")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "No AACPManager available")
|
Log.d(TAG, "AACPManager EQ data empty")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
||||||
@@ -277,18 +276,19 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only show transparency mode section if SDP offset is available
|
// Only show transparency mode section if SDP offset is available
|
||||||
if (isSdpOffsetAvailable.value) {
|
if (BuildConfig.FLAVOR == "xposed") {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.transparency_mode),
|
label = stringResource(R.string.transparency_mode),
|
||||||
checkedState = enabled,
|
checked = enabled.value,
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.customize_transparency_mode_description)
|
description = stringResource(R.string.customize_transparency_mode_description),
|
||||||
|
onCheckedChange = { enabled.value = it }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.amplification),
|
label = stringResource(R.string.amplification),
|
||||||
valueRange = -1f..1f,
|
valueRange = -1f..1f,
|
||||||
mutableFloatState = amplificationSliderValue,
|
value = amplificationSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
amplificationSliderValue.floatValue = it
|
amplificationSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -300,7 +300,7 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.balance),
|
label = stringResource(R.string.balance),
|
||||||
valueRange = -1f..1f,
|
valueRange = -1f..1f,
|
||||||
mutableFloatState = balanceSliderValue,
|
value = balanceSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
balanceSliderValue.floatValue = it
|
balanceSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -313,7 +313,7 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.tone),
|
label = stringResource(R.string.tone),
|
||||||
valueRange = -1f..1f,
|
valueRange = -1f..1f,
|
||||||
mutableFloatState = toneSliderValue,
|
value = toneSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
toneSliderValue.floatValue = it
|
toneSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -325,7 +325,7 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.ambient_noise_reduction),
|
label = stringResource(R.string.ambient_noise_reduction),
|
||||||
valueRange = 0f..1f,
|
valueRange = 0f..1f,
|
||||||
mutableFloatState = ambientNoiseReductionSliderValue,
|
value = ambientNoiseReductionSliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
ambientNoiseReductionSliderValue.floatValue = it
|
ambientNoiseReductionSliderValue.floatValue = it
|
||||||
},
|
},
|
||||||
@@ -336,14 +336,12 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.conversation_boost),
|
label = stringResource(R.string.conversation_boost),
|
||||||
checkedState = conversationBoostEnabled,
|
checked = conversationBoostEnabled.value,
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.conversation_boost_description)
|
description = stringResource(R.string.conversation_boost_description),
|
||||||
|
onCheckedChange = { conversationBoostEnabled.value = it }
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Only show transparency mode EQ section if SDP offset is available
|
|
||||||
if (isSdpOffsetAvailable.value) {
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.equalizer),
|
text = stringResource(R.string.equalizer),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
|||||||
+51
-37
@@ -55,7 +55,6 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
@@ -75,11 +74,8 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
|
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
|
||||||
private const val TAG = "HearingAidAdjustments"
|
private const val TAG = "HearingAidAdjustments"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateHearingTestScreen(@Suppress("unused") navController: NavController) {
|
fun UpdateHearingTestScreen() {
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val attManager = ServiceManager.getService()?.attManager
|
val attManager = ServiceManager.getService()?.attManager
|
||||||
if (attManager == null) {
|
if (attManager == null) {
|
||||||
@@ -138,17 +134,17 @@ fun UpdateHearingTestScreen(@Suppress("unused") navController: NavController) {
|
|||||||
HearingAidSettings(
|
HearingAidSettings(
|
||||||
leftEQ = leftEQ.value,
|
leftEQ = leftEQ.value,
|
||||||
rightEQ = rightEQ.value,
|
rightEQ = rightEQ.value,
|
||||||
leftAmplification = leftAmplification.value,
|
leftAmplification = leftAmplification.floatValue,
|
||||||
rightAmplification = rightAmplification.value,
|
rightAmplification = rightAmplification.floatValue,
|
||||||
leftTone = tone.value,
|
leftTone = tone.floatValue,
|
||||||
rightTone = tone.value,
|
rightTone = tone.floatValue,
|
||||||
leftConversationBoost = conversationBoostEnabled.value,
|
leftConversationBoost = conversationBoostEnabled.value,
|
||||||
rightConversationBoost = conversationBoostEnabled.value,
|
rightConversationBoost = conversationBoostEnabled.value,
|
||||||
leftAmbientNoiseReduction = ambientNoiseReduction.value,
|
leftAmbientNoiseReduction = ambientNoiseReduction.floatValue,
|
||||||
rightAmbientNoiseReduction = ambientNoiseReduction.value,
|
rightAmbientNoiseReduction = ambientNoiseReduction.floatValue,
|
||||||
netAmplification = leftAmplification.value + rightAmplification.value / 2,
|
netAmplification = leftAmplification.floatValue + rightAmplification.floatValue / 2,
|
||||||
balance = 0.5f + (rightAmplification.value - leftAmplification.value) / 2,
|
balance = 0.5f + (rightAmplification.floatValue - leftAmplification.floatValue) / 2,
|
||||||
ownVoiceAmplification = ownVoiceAmplification.value
|
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -161,11 +157,11 @@ fun UpdateHearingTestScreen(@Suppress("unused") navController: NavController) {
|
|||||||
leftEQ.value = parsed.leftEQ.copyOf()
|
leftEQ.value = parsed.leftEQ.copyOf()
|
||||||
rightEQ.value = parsed.rightEQ.copyOf()
|
rightEQ.value = parsed.rightEQ.copyOf()
|
||||||
conversationBoostEnabled.value = parsed.leftConversationBoost
|
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||||
tone.value = parsed.leftTone
|
tone.floatValue = parsed.leftTone
|
||||||
ambientNoiseReduction.value = parsed.leftAmbientNoiseReduction
|
ambientNoiseReduction.floatValue = parsed.leftAmbientNoiseReduction
|
||||||
ownVoiceAmplification.value = parsed.ownVoiceAmplification
|
ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification
|
||||||
leftAmplification.value = parsed.leftAmplification
|
leftAmplification.floatValue = parsed.leftAmplification
|
||||||
rightAmplification.value = parsed.rightAmplification
|
rightAmplification.floatValue = parsed.rightAmplification
|
||||||
Log.d(TAG, "Updated hearing aid settings from notification")
|
Log.d(TAG, "Updated hearing aid settings from notification")
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to parse hearing aid settings from notification")
|
Log.w(TAG, "Failed to parse hearing aid settings from notification")
|
||||||
@@ -181,31 +177,45 @@ fun UpdateHearingTestScreen(@Suppress("unused") navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(leftEQ.value, rightEQ.value, conversationBoostEnabled.value, initialLoadComplete.value, initialReadSucceeded.value, leftAmplification.value, rightAmplification.value, tone.value, ambientNoiseReduction.value, ownVoiceAmplification.value) {
|
LaunchedEffect(
|
||||||
|
leftEQ.value,
|
||||||
|
rightEQ.value,
|
||||||
|
conversationBoostEnabled.value,
|
||||||
|
initialLoadComplete.value,
|
||||||
|
initialReadSucceeded.value,
|
||||||
|
leftAmplification.floatValue,
|
||||||
|
rightAmplification.floatValue,
|
||||||
|
tone.floatValue,
|
||||||
|
ambientNoiseReduction.floatValue,
|
||||||
|
ownVoiceAmplification.floatValue
|
||||||
|
) {
|
||||||
if (!initialLoadComplete.value) {
|
if (!initialLoadComplete.value) {
|
||||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initialReadSucceeded.value) {
|
if (!initialReadSucceeded.value) {
|
||||||
Log.d(TAG, "Initial device read not successful yet - skipping send until read succeeds")
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Initial device read not successful yet - skipping send until read succeeds"
|
||||||
|
)
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
hearingAidSettings.value = HearingAidSettings(
|
hearingAidSettings.value = HearingAidSettings(
|
||||||
leftEQ = leftEQ.value,
|
leftEQ = leftEQ.value,
|
||||||
rightEQ = rightEQ.value,
|
rightEQ = rightEQ.value,
|
||||||
leftAmplification = leftAmplification.value,
|
leftAmplification = leftAmplification.floatValue,
|
||||||
rightAmplification = rightAmplification.value,
|
rightAmplification = rightAmplification.floatValue,
|
||||||
leftTone = tone.value,
|
leftTone = tone.floatValue,
|
||||||
rightTone = tone.value,
|
rightTone = tone.floatValue,
|
||||||
leftConversationBoost = conversationBoostEnabled.value,
|
leftConversationBoost = conversationBoostEnabled.value,
|
||||||
rightConversationBoost = conversationBoostEnabled.value,
|
rightConversationBoost = conversationBoostEnabled.value,
|
||||||
leftAmbientNoiseReduction = ambientNoiseReduction.value,
|
leftAmbientNoiseReduction = ambientNoiseReduction.floatValue,
|
||||||
rightAmbientNoiseReduction = ambientNoiseReduction.value,
|
rightAmbientNoiseReduction = ambientNoiseReduction.floatValue,
|
||||||
netAmplification = leftAmplification.value + rightAmplification.value / 2,
|
netAmplification = leftAmplification.floatValue + rightAmplification.floatValue / 2,
|
||||||
balance = 0.5f + (rightAmplification.value - leftAmplification.value) / 2,
|
balance = 0.5f + (rightAmplification.floatValue - leftAmplification.floatValue) / 2,
|
||||||
ownVoiceAmplification = ownVoiceAmplification.value
|
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||||
)
|
)
|
||||||
Log.d(TAG, "Updated settings: ${hearingAidSettings.value}")
|
Log.d(TAG, "Updated settings: ${hearingAidSettings.value}")
|
||||||
sendHearingAidSettings(attManager, hearingAidSettings.value, debounceJob)
|
sendHearingAidSettings(attManager, hearingAidSettings.value, debounceJob)
|
||||||
@@ -240,14 +250,17 @@ fun UpdateHearingTestScreen(@Suppress("unused") navController: NavController) {
|
|||||||
leftEQ.value = parsedSettings.leftEQ.copyOf()
|
leftEQ.value = parsedSettings.leftEQ.copyOf()
|
||||||
rightEQ.value = parsedSettings.rightEQ.copyOf()
|
rightEQ.value = parsedSettings.rightEQ.copyOf()
|
||||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||||
tone.value = parsedSettings.leftTone
|
tone.floatValue = parsedSettings.leftTone
|
||||||
ambientNoiseReduction.value = parsedSettings.leftAmbientNoiseReduction
|
ambientNoiseReduction.floatValue = parsedSettings.leftAmbientNoiseReduction
|
||||||
ownVoiceAmplification.value = parsedSettings.ownVoiceAmplification
|
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
||||||
leftAmplification.value = parsedSettings.leftAmplification
|
leftAmplification.floatValue = parsedSettings.leftAmplification
|
||||||
rightAmplification.value = parsedSettings.rightAmplification
|
rightAmplification.floatValue = parsedSettings.rightAmplification
|
||||||
initialReadSucceeded.value = true
|
initialReadSucceeded.value = true
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Failed to read/parse initial hearing aid settings after ${initialReadAttempts.intValue} attempts")
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Failed to read/parse initial hearing aid settings after ${initialReadAttempts.intValue} attempts"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -256,7 +269,8 @@ fun UpdateHearingTestScreen(@Suppress("unused") navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val frequencies = listOf("250Hz", "500Hz", "1kHz", "2kHz", "3kHz", "4kHz", "6kHz", "8kHz")
|
val frequencies =
|
||||||
|
listOf("250Hz", "500Hz", "1kHz", "2kHz", "3kHz", "4kHz", "6kHz", "8kHz")
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
@@ -19,22 +19,22 @@
|
|||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -45,36 +45,23 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VersionScreen(navController: NavController) {
|
fun VersionScreen(viewModel: AirPodsViewModel) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val service = ServiceManager.getService()
|
|
||||||
if (service == null) return
|
|
||||||
val airpodsInstance = service.airpodsInstance
|
|
||||||
if (airpodsInstance == null) return
|
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.customize_adaptive_audio)
|
title = stringResource(R.string.version)
|
||||||
) { spacerHeight ->
|
) { spacerHeight ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -120,7 +107,7 @@ fun VersionScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = airpodsInstance.version1 ?: "N/A",
|
text = state.version1,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor.copy(0.8f),
|
color = textColor.copy(0.8f),
|
||||||
@@ -149,7 +136,7 @@ fun VersionScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = airpodsInstance.version2 ?: "N/A",
|
text = state.version2,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor.copy(0.8f),
|
color = textColor.copy(0.8f),
|
||||||
@@ -178,7 +165,7 @@ fun VersionScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = airpodsInstance.version3 ?: "N/A",
|
text = state.version3,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor.copy(0.8f),
|
color = textColor.copy(0.8f),
|
||||||
|
|||||||
@@ -244,8 +244,10 @@ class AirPodsQSService : TileService() {
|
|||||||
|
|
||||||
private fun getNextAncMode(): Int {
|
private fun getNextAncMode(): Int {
|
||||||
val availableModes = getAvailableModes()
|
val availableModes = getAvailableModes()
|
||||||
|
Log.d("AirPodsQSService", "availableModes: $availableModes, currentAncMode: $currentAncMode")
|
||||||
val currentIndex = availableModes.indexOf(currentAncMode)
|
val currentIndex = availableModes.indexOf(currentAncMode)
|
||||||
val nextIndex = (currentIndex + 1) % availableModes.size
|
val nextIndex = (currentIndex + 1) % availableModes.size
|
||||||
|
Log.d("AirPodsQSService", "nextIndex: $nextIndex")
|
||||||
return availableModes[nextIndex]
|
return availableModes[nextIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@ class AACPManager {
|
|||||||
const val TIPI_3: Byte = 0x0C // Don't know this one
|
const val TIPI_3: Byte = 0x0C // Don't know this one
|
||||||
const val SMART_ROUTING_RESP: Byte = 0x11
|
const val SMART_ROUTING_RESP: Byte = 0x11
|
||||||
const val SEND_CONNECTED_MAC: Byte = 0x14
|
const val SEND_CONNECTED_MAC: Byte = 0x14
|
||||||
|
const val AUDIO_SOURCE_2: Byte = 0x0C // seems redundant?
|
||||||
}
|
}
|
||||||
|
|
||||||
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
|
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
|
||||||
@@ -216,7 +217,7 @@ class AACPManager {
|
|||||||
var audioSource: AudioSource? = null
|
var audioSource: AudioSource? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var eqData = FloatArray(8) { 0.0f }
|
var eqData = FloatArray(8)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var eqOnPhone: Boolean = false
|
var eqOnPhone: Boolean = false
|
||||||
@@ -265,6 +266,7 @@ class AACPManager {
|
|||||||
fun onConnectedDevicesReceived(connectedDevices: List<ConnectedDevice>)
|
fun onConnectedDevicesReceived(connectedDevices: List<ConnectedDevice>)
|
||||||
fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean)
|
fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean)
|
||||||
fun onShowNearbyUI(sender: String)
|
fun onShowNearbyUI(sender: String)
|
||||||
|
fun onEQPacketReceived(eqData: FloatArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseStemPressResponse(data: ByteArray): Pair<StemPressType, StemPressBudType> {
|
fun parseStemPressResponse(data: ByteArray): Pair<StemPressType, StemPressBudType> {
|
||||||
@@ -458,21 +460,27 @@ class AACPManager {
|
|||||||
controlCommand.value.joinToString(" ") { "%02X".format(it) }
|
controlCommand.value.joinToString(" ") { "%02X".format(it) }
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
Log.d(
|
|
||||||
TAG, "Control command list is now: ${
|
val controlCommandListText = try {
|
||||||
controlCommandStatusList.joinToString(", ") { it ->
|
controlCommandStatusList.joinToString(", ") { it ->
|
||||||
"${it.identifier.name} (${it.identifier.value.toHexString()}) - ${
|
"${it.identifier.name} (${it.identifier.value.toHexString()}) - ${
|
||||||
it.value.joinToString(
|
it.value.joinToString(
|
||||||
" "
|
" "
|
||||||
) { "%02X".format(it) }
|
) { "%02X".format(it) }
|
||||||
}"
|
}"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.message
|
||||||
}
|
}
|
||||||
}")
|
|
||||||
|
Log.d(
|
||||||
|
TAG, "Control command list is now: $controlCommandListText")
|
||||||
|
|
||||||
val controlCommandIdentifier =
|
val controlCommandIdentifier =
|
||||||
ControlCommandIdentifiers.fromByte(controlCommand.identifier)
|
ControlCommandIdentifiers.fromByte(controlCommand.identifier)
|
||||||
if (controlCommandIdentifier != null) {
|
if (controlCommandIdentifier != null) {
|
||||||
controlCommandListeners[controlCommandIdentifier]?.forEach { listener ->
|
controlCommandListeners[controlCommandIdentifier]?.forEach { listener ->
|
||||||
|
Log.d(TAG, "calling listener for ${controlCommandIdentifier.name}")
|
||||||
listener.onControlCommandReceived(controlCommand)
|
listener.onControlCommandReceived(controlCommand)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -585,7 +593,7 @@ class AACPManager {
|
|||||||
|
|
||||||
eqOnMedia = (packet[10] == 0x01.toByte())
|
eqOnMedia = (packet[10] == 0x01.toByte())
|
||||||
eqOnPhone = (packet[11] == 0x01.toByte())
|
eqOnPhone = (packet[11] == 0x01.toByte())
|
||||||
// there are 4 eqs. i am not sure what those are for, maybe all 4 listening modes, or maybe phone+media left+right, but then there shouldn't be another flag for phone/media enabled. just directly the EQ... weird.
|
// there are 4 eqs. i am not sure what those are for, maybe all 4 listening modes, or maybe phone+media left+right, but then there shouldn't be another flag for phone/media visible. just directly the EQ... weird.
|
||||||
// the EQs are little endian floats
|
// the EQs are little endian floats
|
||||||
val eq1 = ByteBuffer.wrap(packet, 12, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
val eq1 = ByteBuffer.wrap(packet, 12, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
@@ -594,11 +602,14 @@ class AACPManager {
|
|||||||
|
|
||||||
// for now, taking just the first EQ
|
// for now, taking just the first EQ
|
||||||
eqData = FloatArray(8) { i -> eq1.get(i) }
|
eqData = FloatArray(8) { i -> eq1.get(i) }
|
||||||
|
|
||||||
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
|
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
|
||||||
|
|
||||||
|
callback?.onEQPacketReceived(eqData)
|
||||||
}
|
}
|
||||||
|
|
||||||
Opcodes.INFORMATION -> {
|
Opcodes.INFORMATION -> {
|
||||||
Log.e(TAG, "Parsing Information Packet")
|
Log.d(TAG, "Parsing Information Packet")
|
||||||
val information = parseInformationPacket(packet)
|
val information = parseInformationPacket(packet)
|
||||||
callback?.onDeviceInformationReceived(information)
|
callback?.onDeviceInformationReceived(information)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
/*
|
|
||||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
|
||||||
Copyright (C) 2025 LibrePods contributors
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.bluetooth.BluetoothServerSocket
|
|
||||||
import android.bluetooth.BluetoothSocket
|
|
||||||
import android.bluetooth.le.AdvertiseCallback
|
|
||||||
import android.bluetooth.le.AdvertiseData
|
|
||||||
import android.bluetooth.le.AdvertiseSettings
|
|
||||||
import android.bluetooth.le.BluetoothLeAdvertiser
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.ParcelUuid
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
enum class CrossDevicePackets(val packet: ByteArray) {
|
|
||||||
AIRPODS_CONNECTED(byteArrayOf(0x00, 0x01, 0x00, 0x01)),
|
|
||||||
AIRPODS_DISCONNECTED(byteArrayOf(0x00, 0x01, 0x00, 0x00)),
|
|
||||||
REQUEST_DISCONNECT(byteArrayOf(0x00, 0x02, 0x00, 0x00)),
|
|
||||||
REQUEST_BATTERY_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x01)),
|
|
||||||
REQUEST_ANC_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x02)),
|
|
||||||
REQUEST_CONNECTION_STATUS(byteArrayOf(0x00, 0x02, 0x00, 0x03)),
|
|
||||||
AIRPODS_DATA_HEADER(byteArrayOf(0x00, 0x04, 0x00, 0x01)),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
object CrossDevice {
|
|
||||||
var initialized = false
|
|
||||||
private val uuid = UUID.fromString("1abbb9a4-10e4-4000-a75c-8953c5471342")
|
|
||||||
private var serverSocket: BluetoothServerSocket? = null
|
|
||||||
private var clientSocket: BluetoothSocket? = null
|
|
||||||
private lateinit var bluetoothAdapter: BluetoothAdapter
|
|
||||||
private lateinit var bluetoothLeAdvertiser: BluetoothLeAdvertiser
|
|
||||||
private const val MANUFACTURER_ID = 0x1234
|
|
||||||
private const val MANUFACTURER_DATA = "ALN_AirPods"
|
|
||||||
var isAvailable: Boolean = false // set to true when airpods are connected to another device
|
|
||||||
var batteryBytes: ByteArray = byteArrayOf()
|
|
||||||
var ancBytes: ByteArray = byteArrayOf()
|
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
|
||||||
private const val PACKET_LOG_KEY = "packet_log"
|
|
||||||
private var earDetectionStatus = listOf(false, false)
|
|
||||||
var disconnectionRequested = false
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
fun init(context: Context) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
Log.d("CrossDevice", "Initializing CrossDevice")
|
|
||||||
sharedPreferences = context.getSharedPreferences("packet_logs", Context.MODE_PRIVATE)
|
|
||||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)}
|
|
||||||
this@CrossDevice.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
|
||||||
this@CrossDevice.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
|
||||||
// startAdvertising()
|
|
||||||
startServer()
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
private fun startServer() {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
if (!bluetoothAdapter.isEnabled) return@launch
|
|
||||||
// serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord("ALNCrossDevice", uuid)
|
|
||||||
Log.d("CrossDevice", "Server started")
|
|
||||||
while (serverSocket != null) {
|
|
||||||
if (!bluetoothAdapter.isEnabled) {
|
|
||||||
serverSocket?.close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (clientSocket != null) {
|
|
||||||
try {
|
|
||||||
clientSocket!!.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val socket = serverSocket!!.accept()
|
|
||||||
handleClientConnection(socket)
|
|
||||||
} catch (e: IOException) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "unused")
|
|
||||||
private fun startAdvertising() {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val settings = AdvertiseSettings.Builder()
|
|
||||||
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
||||||
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
|
||||||
.setConnectable(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val data = AdvertiseData.Builder()
|
|
||||||
.setIncludeDeviceName(true)
|
|
||||||
.addManufacturerData(MANUFACTURER_ID, MANUFACTURER_DATA.toByteArray())
|
|
||||||
.addServiceUuid(ParcelUuid(uuid))
|
|
||||||
.build()
|
|
||||||
try {
|
|
||||||
bluetoothLeAdvertiser.startAdvertising(settings, data, advertiseCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CrossDevice", "Failed to start BLE Advertising: ${e.message}")
|
|
||||||
}
|
|
||||||
Log.d("CrossDevice", "BLE Advertising started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val advertiseCallback = object : AdvertiseCallback() {
|
|
||||||
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
||||||
Log.d("CrossDevice", "BLE Advertising started successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartFailure(errorCode: Int) {
|
|
||||||
Log.e("CrossDevice", "BLE Advertising failed with error code: $errorCode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAirPodsConnected(connected: Boolean) {
|
|
||||||
if (connected) {
|
|
||||||
isAvailable = false
|
|
||||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)}
|
|
||||||
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_CONNECTED.packet)
|
|
||||||
} else {
|
|
||||||
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)
|
|
||||||
// Reset state variables
|
|
||||||
isAvailable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendReceivedPacket(packet: ByteArray) {
|
|
||||||
if (clientSocket == null || clientSocket!!.outputStream != null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logPacket(packet: ByteArray, source: String) {
|
|
||||||
val packetHex = packet.joinToString(" ") { "%02X".format(it) }
|
|
||||||
val logEntry = "$source: $packetHex"
|
|
||||||
val logs = sharedPreferences.getStringSet(PACKET_LOG_KEY, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
|
|
||||||
logs.add(logEntry)
|
|
||||||
sharedPreferences.edit { putStringSet(PACKET_LOG_KEY, logs)}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
private fun handleClientConnection(socket: BluetoothSocket) {
|
|
||||||
Log.d("CrossDevice", "Client connected")
|
|
||||||
notifyAirPodsConnectedRemotely(ServiceManager.getService()?.applicationContext!!)
|
|
||||||
clientSocket = socket
|
|
||||||
val inputStream = socket.inputStream
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var bytes: Int
|
|
||||||
setAirPodsConnected(ServiceManager.getService()?.isConnectedLocally == true)
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
bytes = inputStream.read(buffer)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
notifyAirPodsDisconnectedRemotely(ServiceManager.getService()?.applicationContext!!)
|
|
||||||
val s = serverSocket?.accept()
|
|
||||||
if (s != null) {
|
|
||||||
handleClientConnection(s)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
var packet = buffer.copyOf(bytes)
|
|
||||||
logPacket(packet, "Relay")
|
|
||||||
Log.d("CrossDevice", "Received packet: ${packet.joinToString("") { "%02x".format(it) }}")
|
|
||||||
if (bytes == -1) {
|
|
||||||
notifyAirPodsDisconnectedRemotely(ServiceManager.getService()?.applicationContext!!)
|
|
||||||
break
|
|
||||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_DISCONNECT.packet) || packet.contentEquals(CrossDevicePackets.REQUEST_DISCONNECT.packet + CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
|
|
||||||
ServiceManager.getService()?.disconnectForCD()
|
|
||||||
disconnectionRequested = true
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
delay(1000)
|
|
||||||
disconnectionRequested = false
|
|
||||||
}
|
|
||||||
} else if (packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)) {
|
|
||||||
isAvailable = true
|
|
||||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", true)}
|
|
||||||
} else if (packet.contentEquals(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)) {
|
|
||||||
isAvailable = false
|
|
||||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)}
|
|
||||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_BATTERY_BYTES.packet)) {
|
|
||||||
Log.d("CrossDevice", "Received battery request, battery data: ${batteryBytes.joinToString("") { "%02x".format(it) }}")
|
|
||||||
sendRemotePacket(batteryBytes)
|
|
||||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_ANC_BYTES.packet)) {
|
|
||||||
Log.d("CrossDevice", "Received ANC request")
|
|
||||||
sendRemotePacket(ancBytes)
|
|
||||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_CONNECTION_STATUS.packet)) {
|
|
||||||
Log.d("CrossDevice", "Received connection status request")
|
|
||||||
sendRemotePacket(if (ServiceManager.getService()?.isConnectedLocally == true) CrossDevicePackets.AIRPODS_CONNECTED.packet else CrossDevicePackets.AIRPODS_DISCONNECTED.packet)
|
|
||||||
} else {
|
|
||||||
if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
|
|
||||||
isAvailable = true
|
|
||||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", true) }
|
|
||||||
if (packet.size % 2 == 0) {
|
|
||||||
val half = packet.size / 2
|
|
||||||
if (packet.sliceArray(0 until half).contentEquals(packet.sliceArray(half until packet.size))) {
|
|
||||||
Log.d("CrossDevice", "Duplicated packet, trimming")
|
|
||||||
packet = packet.sliceArray(0 until half)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var trimmedPacket = packet.drop(CrossDevicePackets.AIRPODS_DATA_HEADER.packet.size).toByteArray()
|
|
||||||
Log.d("CrossDevice", "Received relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}")
|
|
||||||
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
|
||||||
val packetInHex = trimmedPacket.joinToString("") { "%02x".format(it) }
|
|
||||||
// ServiceManager.getService()?.sendPacket(packetInHex)
|
|
||||||
} else if (ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket) == true) {
|
|
||||||
batteryBytes = trimmedPacket
|
|
||||||
ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket)
|
|
||||||
Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}")
|
|
||||||
ServiceManager.getService()?.updateBattery()
|
|
||||||
ServiceManager.getService()?.sendBatteryBroadcast()
|
|
||||||
ServiceManager.getService()?.sendBatteryNotification()
|
|
||||||
} else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) {
|
|
||||||
ServiceManager.getService()?.ancNotification?.setStatus(trimmedPacket)
|
|
||||||
ServiceManager.getService()?.sendANCBroadcast()
|
|
||||||
ServiceManager.getService()?.updateNoiseControlWidget()
|
|
||||||
ancBytes = trimmedPacket
|
|
||||||
} else if (ServiceManager.getService()?.earDetectionNotification?.isEarDetectionData(trimmedPacket) == true) {
|
|
||||||
Log.d("CrossDevice", "Ear detection data: ${trimmedPacket.joinToString("") { "%02x".format(it) }}")
|
|
||||||
ServiceManager.getService()?.earDetectionNotification?.setStatus(trimmedPacket)
|
|
||||||
val newEarDetectionStatus = listOf(
|
|
||||||
ServiceManager.getService()?.earDetectionNotification?.status?.get(0) == 0x00.toByte(),
|
|
||||||
ServiceManager.getService()?.earDetectionNotification?.status?.get(1) == 0x00.toByte()
|
|
||||||
)
|
|
||||||
if (earDetectionStatus == listOf(false, false) && newEarDetectionStatus.contains(true)) {
|
|
||||||
ServiceManager.getService()?.applicationContext?.sendBroadcast(
|
|
||||||
Intent("me.kavishdevar.librepods.cross_device_island")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
earDetectionStatus = newEarDetectionStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendRemotePacket(byteArray: ByteArray) {
|
|
||||||
if (clientSocket == null || clientSocket!!.outputStream == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clientSocket?.outputStream?.write(byteArray)
|
|
||||||
clientSocket?.outputStream?.flush()
|
|
||||||
logPacket(byteArray, "Sent")
|
|
||||||
Log.d("CrossDevice", "Sent packet to remote device")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun notifyAirPodsConnectedRemotely(context: Context) {
|
|
||||||
val intent = Intent("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
|
||||||
context.sendBroadcast(intent)
|
|
||||||
}
|
|
||||||
fun notifyAirPodsDisconnectedRemotely(context: Context) {
|
|
||||||
val intent = Intent("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
|
||||||
context.sendBroadcast(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -240,6 +240,7 @@ class IslandWindow(private val context: Context) {
|
|||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
|
|
||||||
containerView.addView(islandView, containerParams)
|
containerView.addView(islandView, containerParams)
|
||||||
|
|
||||||
params = WindowManager.LayoutParams(
|
params = WindowManager.LayoutParams(
|
||||||
@@ -379,7 +380,11 @@ class IslandWindow(private val context: Context) {
|
|||||||
videoView.start()
|
videoView.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
windowManager.addView(containerView, params)
|
try {
|
||||||
|
windowManager.addView(containerView, params)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
islandView.post {
|
islandView.post {
|
||||||
initialHeight = islandView.height
|
initialHeight = islandView.height
|
||||||
|
|||||||
@@ -139,7 +139,11 @@ class PopupWindow(
|
|||||||
vid.start()
|
vid.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
mWindowManager.addView(mView, mParams)
|
try {
|
||||||
|
mWindowManager.addView(mView, mParams)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
val displayMetrics = mView.context.resources.displayMetrics
|
val displayMetrics = mView.context.resources.displayMetrics
|
||||||
val screenHeight = displayMetrics.heightPixels
|
val screenHeight = displayMetrics.heightPixels
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
|
|
||||||
|
fun isSupported(): Boolean {
|
||||||
|
if (BuildConfig.PLAY_BUILD) {
|
||||||
|
val isPixel = Build.MANUFACTURER.lowercase() == "google"
|
||||||
|
val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
|
||||||
|
|
||||||
|
if (isPixel) {
|
||||||
|
when (Build.VERSION.SDK_INT) {
|
||||||
|
36 -> {
|
||||||
|
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005"
|
||||||
|
}
|
||||||
|
|
||||||
|
37 -> {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isOppoOrOnePlus) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*fun isSupported(): Boolean {
|
||||||
|
return true
|
||||||
|
}*/
|
||||||
@@ -139,7 +139,7 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings {
|
|||||||
|
|
||||||
private var debounceJob: Job? = null
|
private var debounceJob: Job? = null
|
||||||
|
|
||||||
fun sendTransparencySettings(attManager: ATTManager, transparencySettings: TransparencySettings) {
|
fun sendTransparencySettings(writer: (ATTHandles, ByteArray) -> Unit, transparencySettings: TransparencySettings) {
|
||||||
debounceJob?.cancel()
|
debounceJob?.cancel()
|
||||||
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
delay(100)
|
delay(100)
|
||||||
@@ -171,7 +171,7 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans
|
|||||||
}
|
}
|
||||||
|
|
||||||
val data = buffer.array()
|
val data = buffer.array()
|
||||||
attManager.write(ATTHandles.TRANSPARENCY, value = data)
|
writer(ATTHandles.TRANSPARENCY, data)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
|
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||||
|
import me.kavishdevar.librepods.constants.Battery
|
||||||
|
import me.kavishdevar.librepods.constants.StemAction
|
||||||
|
import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||||
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers
|
||||||
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
|
import me.kavishdevar.librepods.utils.AirPodsInstance
|
||||||
|
import me.kavishdevar.librepods.utils.AirPodsModels
|
||||||
|
import me.kavishdevar.librepods.utils.Capability
|
||||||
|
|
||||||
|
@Suppress("ArrayInDataClass")
|
||||||
|
data class AirPodsUiState(
|
||||||
|
val deviceName: String,
|
||||||
|
|
||||||
|
val isLocallyConnected: Boolean = false,
|
||||||
|
|
||||||
|
val instance: AirPodsInstance? = null,
|
||||||
|
val capabilities: Set<Capability> = emptySet(),
|
||||||
|
|
||||||
|
val controlStates: Map<ControlCommandIdentifiers, ByteArray> = emptyMap(),
|
||||||
|
val offListeningMode: Boolean = true,
|
||||||
|
|
||||||
|
val battery: List<Battery> = emptyList(),
|
||||||
|
val ancMode: Int = 3,
|
||||||
|
|
||||||
|
val modelName: String = "",
|
||||||
|
val actualModel: String = "",
|
||||||
|
val serialNumbers: List<String> = emptyList(),
|
||||||
|
val version1: String = "",
|
||||||
|
val version2: String = "",
|
||||||
|
val version3: String = "",
|
||||||
|
|
||||||
|
val headTrackingActive: Boolean = false,
|
||||||
|
val headGesturesEnabled: Boolean = true,
|
||||||
|
|
||||||
|
val eqData: FloatArray = floatArrayOf(),
|
||||||
|
|
||||||
|
val automaticEarDetectionEnabled: Boolean = true,
|
||||||
|
val automaticConnectionEnabled: Boolean = true,
|
||||||
|
|
||||||
|
val leftAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||||
|
val rightAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||||
|
|
||||||
|
val isPremium: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
class AirPodsViewModel(
|
||||||
|
private val service: AirPodsService,
|
||||||
|
private val sharedPreferences: SharedPreferences,
|
||||||
|
private val controlRepo: ControlCommandRepository,
|
||||||
|
private val appContext: Context
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(AirPodsUiState(deviceName = sharedPreferences.getString("name", "AirPods Pro") ?: "AirPods Pro"))
|
||||||
|
val uiState: StateFlow<AirPodsUiState> = _uiState
|
||||||
|
|
||||||
|
private val listeners = mutableMapOf<
|
||||||
|
ControlCommandIdentifiers,
|
||||||
|
AACPManager.ControlCommandListener
|
||||||
|
>()
|
||||||
|
|
||||||
|
private lateinit var broadcastReceiver: BroadcastReceiver
|
||||||
|
|
||||||
|
private val _cameraAction = MutableStateFlow(
|
||||||
|
sharedPreferences.getString("camera_action", null)
|
||||||
|
?.let { value -> AACPManager.Companion.StemPressType.entries.find { it.name == value } }
|
||||||
|
)
|
||||||
|
|
||||||
|
val cameraAction: StateFlow<AACPManager.Companion.StemPressType?> = _cameraAction
|
||||||
|
|
||||||
|
fun setCameraAction(action: AACPManager.Companion.StemPressType?) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
if (action == null) remove("camera_action")
|
||||||
|
else putString("camera_action", action.name)
|
||||||
|
}
|
||||||
|
_cameraAction.value = action
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeBroadcasts()
|
||||||
|
loadName()
|
||||||
|
loadInstance()
|
||||||
|
loadSharedPreferences()
|
||||||
|
setupControlObservers()
|
||||||
|
observeBilling()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
listeners.forEach { (id, listener) ->
|
||||||
|
controlRepo.remove(id, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
appContext.unregisterReceiver(broadcastReceiver)
|
||||||
|
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadName() {
|
||||||
|
val name = sharedPreferences.getString("name", "AirPods Pro")!!
|
||||||
|
_uiState.update { it.copy(deviceName = name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeBilling() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
BillingManager.provider.isPremium.collect { premium ->
|
||||||
|
|
||||||
|
if (!premium) {
|
||||||
|
setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, false)
|
||||||
|
setHeadGesturesEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isPremium = premium) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeBroadcasts() {
|
||||||
|
broadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
AirPodsNotifications.AIRPODS_CONNECTED -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isLocallyConnected = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AirPodsNotifications.AIRPODS_DISCONNECTED -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isLocallyConnected = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AirPodsNotifications.BATTERY_DATA -> {
|
||||||
|
val data = intent.getParcelableArrayListExtra("data", Battery::class.java)?.toList() ?: emptyList()
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(battery = data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AirPodsNotifications.EQ_DATA -> {
|
||||||
|
val data = intent.getFloatArrayExtra("eqData") ?: floatArrayOf()
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(eqData = data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AirPodsNotifications.AIRPODS_INFORMATION_UPDATED -> {
|
||||||
|
loadInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val filter = IntentFilter().apply {
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
|
addAction(AirPodsNotifications.BATTERY_DATA)
|
||||||
|
addAction(AirPodsNotifications.EQ_DATA)
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_INFORMATION_UPDATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
appContext.registerReceiver(
|
||||||
|
broadcastReceiver,
|
||||||
|
filter,
|
||||||
|
Context.RECEIVER_NOT_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControlCommandValue(
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
controlRepo.setValue(identifier, value)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
controlStates = it.controlStates + (identifier to value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControlCommandBoolean(
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
enabled: Boolean
|
||||||
|
) {
|
||||||
|
setControlCommandValue(
|
||||||
|
identifier,
|
||||||
|
if (enabled) byteArrayOf(0x01) else byteArrayOf(0x02)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControlCommandInt(
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
value: Int
|
||||||
|
) {
|
||||||
|
setControlCommandValue(identifier, byteArrayOf(value.toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControlCommandByte(
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
value: Byte
|
||||||
|
) {
|
||||||
|
setControlCommandValue(identifier, byteArrayOf(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeControl(identifier: ControlCommandIdentifiers) {
|
||||||
|
val listener = controlRepo.observe(identifier) { value ->
|
||||||
|
_uiState.update { state ->
|
||||||
|
val current = state.controlStates[identifier]
|
||||||
|
if (current?.contentEquals(value) == true) return@update state
|
||||||
|
|
||||||
|
state.copy(
|
||||||
|
controlStates = state.controlStates + (identifier to value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[identifier] = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// I'm lazy, sorry.
|
||||||
|
fun setupControlObservers() {
|
||||||
|
val identifiersList = listOf(
|
||||||
|
ControlCommandIdentifiers.MIC_MODE,
|
||||||
|
ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
|
||||||
|
ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
|
||||||
|
ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||||
|
ControlCommandIdentifiers.ONE_BUD_ANC_MODE,
|
||||||
|
ControlCommandIdentifiers.LISTENING_MODE,
|
||||||
|
ControlCommandIdentifiers.AUTO_ANSWER_MODE,
|
||||||
|
ControlCommandIdentifiers.CHIME_VOLUME,
|
||||||
|
ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL,
|
||||||
|
ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG,
|
||||||
|
ControlCommandIdentifiers.VOLUME_SWIPE_MODE,
|
||||||
|
ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
||||||
|
ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
||||||
|
ControlCommandIdentifiers.HEARING_AID,
|
||||||
|
ControlCommandIdentifiers.AUTO_ANC_STRENGTH,
|
||||||
|
ControlCommandIdentifiers.HPS_GAIN_SWIPE,
|
||||||
|
ControlCommandIdentifiers.HEARING_ASSIST_CONFIG,
|
||||||
|
ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
||||||
|
ControlCommandIdentifiers.STEM_CONFIG,
|
||||||
|
ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG,
|
||||||
|
ControlCommandIdentifiers.ALLOW_AUTO_CONNECT,
|
||||||
|
ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
||||||
|
ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||||
|
ControlCommandIdentifiers.OWNS_CONNECTION,
|
||||||
|
ControlCommandIdentifiers.PPE_TOGGLE_CONFIG,
|
||||||
|
)
|
||||||
|
for (identifier in identifiersList) {
|
||||||
|
observeControl(identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshInitialData() {
|
||||||
|
service.let { service ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLocallyConnected = service.isConnected(),
|
||||||
|
battery = service.getBattery()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSharedPreferences() {
|
||||||
|
val offListeningModeEnabled = sharedPreferences.getBoolean("off_listening_mode", true)
|
||||||
|
val automaticEarDetectionEnabled = sharedPreferences.getBoolean("automatic_ear_detection", true)
|
||||||
|
val automaticConnectionEnabled = sharedPreferences.getBoolean("automatic_connection_ctrl_cmd", true)
|
||||||
|
val headGesturesEnabled = sharedPreferences.getBoolean("head_gestures", true)
|
||||||
|
val leftAction = StemAction.valueOf(sharedPreferences.getString("left_long_press_action", "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES")
|
||||||
|
val rightAction = StemAction.valueOf(sharedPreferences.getString("right_long_press_action", "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES")
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
offListeningMode = offListeningModeEnabled,
|
||||||
|
automaticEarDetectionEnabled = automaticEarDetectionEnabled,
|
||||||
|
automaticConnectionEnabled = automaticConnectionEnabled,
|
||||||
|
headGesturesEnabled = headGesturesEnabled,
|
||||||
|
leftAction = leftAction,
|
||||||
|
rightAction = rightAction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOffListeningMode(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("off_listening_mode", enabled) }
|
||||||
|
setControlCommandBoolean(ControlCommandIdentifiers.ALLOW_OFF_OPTION, enabled)
|
||||||
|
Log.d("AirPodsViewModel", "Hello???? $enabled")
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(offListeningMode = enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHeadGesturesEnabled(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("head_gestures", enabled) }
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(headGesturesEnabled = enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadInstance() {
|
||||||
|
val instance = service.airpodsInstance ?: AirPodsInstance(
|
||||||
|
name = "AirPods",
|
||||||
|
model = AirPodsModels.getModelByModelNumber("A3049")!!,
|
||||||
|
actualModelNumber = "A3049",
|
||||||
|
aacpManager = service.aacpManager,
|
||||||
|
serialNumber = null,
|
||||||
|
leftSerialNumber = null,
|
||||||
|
rightSerialNumber = null,
|
||||||
|
version1 = null,
|
||||||
|
version2 = null,
|
||||||
|
version3 = null,
|
||||||
|
attManager = null
|
||||||
|
)
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
capabilities = instance.model.capabilities,
|
||||||
|
instance = instance,
|
||||||
|
modelName = instance.model.displayName,
|
||||||
|
actualModel = instance.actualModelNumber,
|
||||||
|
serialNumbers = listOf(instance.serialNumber ?: "", instance.leftSerialNumber ?: "", instance.rightSerialNumber ?: ""),
|
||||||
|
version1 = instance.version1 ?: "",
|
||||||
|
version2 = instance.version2 ?: "",
|
||||||
|
version3 = instance.version3 ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reconnectFromSavedMac() {
|
||||||
|
service.reconnectFromSavedMac()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setName(name: String) {
|
||||||
|
service.setName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startHeadTracking() {
|
||||||
|
service.startHeadTracking()
|
||||||
|
_uiState.update { it.copy(headTrackingActive = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopHeadTracking() {
|
||||||
|
service.stopHeadTracking()
|
||||||
|
_uiState.update { it.copy(headTrackingActive = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setATTCharacteristicValue(handle: ATTHandles, value: ByteArray) {
|
||||||
|
service.attManager?.write(handle, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getATTCharacteristicValue(handle: ATTHandles): ByteArray? {
|
||||||
|
return service.attManager?.read(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutomaticEarDetectionEnabled(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("automatic_ear_detection", enabled) }
|
||||||
|
setControlCommandBoolean(ControlCommandIdentifiers.EAR_DETECTION_CONFIG, enabled)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
automaticEarDetectionEnabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutomaticConnectionEnabled(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("automatic_connection_ctrl_cmd", enabled) }
|
||||||
|
setControlCommandBoolean(ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG, enabled)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
automaticConnectionEnabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purchase(context: Context) {
|
||||||
|
BillingManager.provider.purchase(context as Activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package me.kavishdevar.librepods.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
data class AppSettingsUiState(
|
||||||
|
val showPhoneBatteryInWidget: Boolean = false,
|
||||||
|
val conversationalAwarenessPauseMusicEnabled: Boolean = false,
|
||||||
|
val relativeConversationalAwarenessVolumeEnabled: Boolean = true,
|
||||||
|
val disconnectWhenNotWearing: Boolean = false,
|
||||||
|
val takeoverWhenDisconnected: Boolean = false,
|
||||||
|
val takeoverWhenIdle: Boolean = false,
|
||||||
|
val takeoverWhenMusic: Boolean = false,
|
||||||
|
val takeoverWhenCall: Boolean = false,
|
||||||
|
val takeoverWhenRingingCall: Boolean = false,
|
||||||
|
val takeoverWhenMediaStart: Boolean = false,
|
||||||
|
val useAlternateHeadTrackingPackets: Boolean = true,
|
||||||
|
val conversationalAwarenessVolume: Float = 43f,
|
||||||
|
val showCameraDialog: Boolean = false,
|
||||||
|
val cameraPackageValue: String = "",
|
||||||
|
val cameraPackageError: String? = null,
|
||||||
|
val isPremium: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
class AppSettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(AppSettingsUiState())
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadSettings()
|
||||||
|
observeBilling()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeBilling() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
BillingManager.provider.isPremium.collect { premium ->
|
||||||
|
_uiState.update { it.copy(isPremium = premium) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
showPhoneBatteryInWidget = sharedPreferences.getBoolean("show_phone_battery_in_widget", false),
|
||||||
|
conversationalAwarenessPauseMusicEnabled = sharedPreferences.getBoolean("conversational_awareness_pause_music", false),
|
||||||
|
relativeConversationalAwarenessVolumeEnabled = sharedPreferences.getBoolean("relative_conversational_awareness_volume", true),
|
||||||
|
disconnectWhenNotWearing = sharedPreferences.getBoolean("disconnect_when_not_wearing", false),
|
||||||
|
takeoverWhenDisconnected = sharedPreferences.getBoolean("takeover_when_disconnected", false),
|
||||||
|
takeoverWhenIdle = sharedPreferences.getBoolean("takeover_when_idle", false),
|
||||||
|
takeoverWhenMusic = sharedPreferences.getBoolean("takeover_when_music", false),
|
||||||
|
takeoverWhenCall = sharedPreferences.getBoolean("takeover_when_call", false),
|
||||||
|
takeoverWhenRingingCall = sharedPreferences.getBoolean("takeover_when_ringing_call", false),
|
||||||
|
takeoverWhenMediaStart = sharedPreferences.getBoolean("takeover_when_media_start", false),
|
||||||
|
useAlternateHeadTrackingPackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", true),
|
||||||
|
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
|
||||||
|
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShowPhoneBatteryInWidget(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("show_phone_battery_in_widget", enabled) }
|
||||||
|
_uiState.update { it.copy(showPhoneBatteryInWidget = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConversationalAwarenessPauseMusicEnabled(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("conversational_awareness_pause_music", enabled) }
|
||||||
|
_uiState.update { it.copy(conversationalAwarenessPauseMusicEnabled = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRelativeConversationalAwarenessVolumeEnabled(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("relative_conversational_awareness_volume", enabled) }
|
||||||
|
_uiState.update { it.copy(relativeConversationalAwarenessVolumeEnabled = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDisconnectWhenNotWearing(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("disconnect_when_not_wearing", enabled) }
|
||||||
|
_uiState.update { it.copy(disconnectWhenNotWearing = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTakeoverWhenDisconnected(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("takeover_when_disconnected", enabled) }
|
||||||
|
_uiState.update { it.copy(takeoverWhenDisconnected = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTakeoverWhenIdle(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("takeover_when_idle", enabled) }
|
||||||
|
_uiState.update { it.copy(takeoverWhenIdle = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTakeoverWhenMusic(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("takeover_when_music", enabled) }
|
||||||
|
_uiState.update { it.copy(takeoverWhenMusic = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTakeoverWhenCall(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("takeover_when_call", enabled) }
|
||||||
|
_uiState.update { it.copy(takeoverWhenCall = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTakeoverWhenRingingCall(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("takeover_when_ringing_call", enabled) }
|
||||||
|
_uiState.update { it.copy(takeoverWhenRingingCall = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTakeoverWhenMediaStart(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("takeover_when_media_start", enabled) }
|
||||||
|
_uiState.update { it.copy(takeoverWhenMediaStart = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUseAlternateHeadTrackingPackets(enabled: Boolean) {
|
||||||
|
sharedPreferences.edit { putBoolean("use_alternate_head_tracking_packets", enabled) }
|
||||||
|
_uiState.update { it.copy(useAlternateHeadTrackingPackets = enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConversationalAwarenessVolume(volume: Float) {
|
||||||
|
sharedPreferences.edit { putInt("conversational_awareness_volume", volume.roundToInt()) }
|
||||||
|
_uiState.update { it.copy(conversationalAwarenessVolume = volume) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShowCameraDialog(show: Boolean) {
|
||||||
|
_uiState.update { it.copy(showCameraDialog = show) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCameraPackageValue(value: String) {
|
||||||
|
_uiState.update { it.copy(cameraPackageValue = value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCameraPackageError(error: String?) {
|
||||||
|
_uiState.update { it.copy(cameraPackageError = error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCameraPackage() {
|
||||||
|
if (_uiState.value.cameraPackageValue.isBlank()) {
|
||||||
|
sharedPreferences.edit { remove("custom_camera_package") }
|
||||||
|
} else {
|
||||||
|
sharedPreferences.edit { putString("custom_camera_package", _uiState.value.cameraPackageValue) }
|
||||||
|
}
|
||||||
|
setShowCameraDialog(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purchase(context: Context) {
|
||||||
|
BillingManager.provider.purchase(context as Activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Rumore Ambientale</string>
|
<string name="environmental_noise">Rumore Ambientale</string>
|
||||||
<string name="reconnect_to_last_device">Riconnetti all'ultimo dispositivo connesso</string>
|
<string name="reconnect_to_last_device">Riconnetti all'ultimo dispositivo connesso</string>
|
||||||
<string name="disconnect">Disconnetti</string>
|
<string name="disconnect">Disconnetti</string>
|
||||||
<string name="support_me">Supportami</string>
|
|
||||||
<string name="never_show_again">Non mostrare più</string>
|
|
||||||
<string name="support_dialog_description">Di recente ho perso il mio AirPod sinistro. Se hai trovato utile LibrePods, considera di supportarmi su GitHub Sponsors in modo che possa acquistare un sostituto e continuare a lavorare su questo progetto: anche una piccola somma fa molto. Grazie per il tuo supporto!</string>
|
|
||||||
<string name="support_librepods">Supporta LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Disattiva la gestione del rumore</string>
|
<string name="listening_mode_off_description">Disattiva la gestione del rumore</string>
|
||||||
<string name="listening_mode_transparency_description">Lascia entrare i suoni esterni</string>
|
<string name="listening_mode_transparency_description">Lascia entrare i suoni esterni</string>
|
||||||
<string name="listening_mode_adaptive_description">Regola dinamicamente il rumore esterno</string>
|
<string name="listening_mode_adaptive_description">Regola dinamicamente il rumore esterno</string>
|
||||||
|
|||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Ruido ambiental</string>
|
<string name="environmental_noise">Ruido ambiental</string>
|
||||||
<string name="reconnect_to_last_device">Reconectar al último dispositivo conectado</string>
|
<string name="reconnect_to_last_device">Reconectar al último dispositivo conectado</string>
|
||||||
<string name="disconnect">Desconectar</string>
|
<string name="disconnect">Desconectar</string>
|
||||||
<string name="support_me">Apóyame</string>
|
|
||||||
<string name="never_show_again">No volver a mostrar</string>
|
|
||||||
<string name="support_dialog_description">Hace poco perdí mi AirPod izquierdo. Si LibrePods te ha resultado útil, considera apoyarme en GitHub Sponsors para que pueda comprar un reemplazo y seguir trabajando en este proyecto; incluso una pequeña donación es de gran ayuda. ¡Gracias por tu apoyo!</string>
|
|
||||||
<string name="support_librepods">Apoya a LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Desactiva la gestión del ruido</string>
|
<string name="listening_mode_off_description">Desactiva la gestión del ruido</string>
|
||||||
<string name="listening_mode_transparency_description">Deja entrar los sonidos externos</string>
|
<string name="listening_mode_transparency_description">Deja entrar los sonidos externos</string>
|
||||||
<string name="listening_mode_adaptive_description">Ajuste dinámico del ruido externo</string>
|
<string name="listening_mode_adaptive_description">Ajuste dinámico del ruido externo</string>
|
||||||
|
|||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Bruit environnemental</string>
|
<string name="environmental_noise">Bruit environnemental</string>
|
||||||
<string name="reconnect_to_last_device">Reconnecter au dernier appareil</string>
|
<string name="reconnect_to_last_device">Reconnecter au dernier appareil</string>
|
||||||
<string name="disconnect">Déconnecter</string>
|
<string name="disconnect">Déconnecter</string>
|
||||||
<string name="support_me">Soutenez-moi</string>
|
|
||||||
<string name="never_show_again">Ne plus afficher</string>
|
|
||||||
<string name="support_dialog_description">J\'ai récemment perdu mon AirPod gauche. Si LibrePods vous est utile, pensez à me soutenir sur GitHub Sponsors pour m\'aider à en racheter un et continuer ce projet — même un petit montant aide beaucoup. Merci pour votre soutien !</string>
|
|
||||||
<string name="support_librepods">Soutenir LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Désactiver la gestion du bruit</string>
|
<string name="listening_mode_off_description">Désactiver la gestion du bruit</string>
|
||||||
<string name="listening_mode_transparency_description">Laisser entrer les sons extérieurs</string>
|
<string name="listening_mode_transparency_description">Laisser entrer les sons extérieurs</string>
|
||||||
<string name="listening_mode_adaptive_description">Ajuster dynamiquement les sons extérieurs</string>
|
<string name="listening_mode_adaptive_description">Ajuster dynamiquement les sons extérieurs</string>
|
||||||
|
|||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Ruído Ambiental</string>
|
<string name="environmental_noise">Ruído Ambiental</string>
|
||||||
<string name="reconnect_to_last_device">Reconectar ao último dispositivo conectado</string>
|
<string name="reconnect_to_last_device">Reconectar ao último dispositivo conectado</string>
|
||||||
<string name="disconnect">Desconectar</string>
|
<string name="disconnect">Desconectar</string>
|
||||||
<string name="support_me">Me Apoiar</string>
|
|
||||||
<string name="never_show_again">Nunca mostrar novamente</string>
|
|
||||||
<string name="support_dialog_description">Recentemente perdi meu AirPod esquerdo. Se você achou o LibrePods útil, considere me apoiar no GitHub Sponsors para que eu possa comprar uma substituição e continuar trabalhando neste projeto - mesmo uma pequena quantia faz muita diferença. Obrigado pelo seu apoio!</string>
|
|
||||||
<string name="support_librepods">Apoiar LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Desativa o gerenciamento de ruído</string>
|
<string name="listening_mode_off_description">Desativa o gerenciamento de ruído</string>
|
||||||
<string name="listening_mode_transparency_description">Permite sons externos</string>
|
<string name="listening_mode_transparency_description">Permite sons externos</string>
|
||||||
<string name="listening_mode_adaptive_description">Ajusta dinamicamente o ruído externo</string>
|
<string name="listening_mode_adaptive_description">Ajusta dinamicamente o ruído externo</string>
|
||||||
|
|||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Çevresel Gürültü</string>
|
<string name="environmental_noise">Çevresel Gürültü</string>
|
||||||
<string name="reconnect_to_last_device">Son bağlanan cihaza yeniden bağlan</string>
|
<string name="reconnect_to_last_device">Son bağlanan cihaza yeniden bağlan</string>
|
||||||
<string name="disconnect">Bağlantıyı Kes</string>
|
<string name="disconnect">Bağlantıyı Kes</string>
|
||||||
<string name="support_me">Beni destekle</string>
|
|
||||||
<string name="never_show_again">Bir daha gösterme</string>
|
|
||||||
<string name="support_dialog_description">Yakın zamanda sol AirPod\'umu kaybettim. LibrePods\'u faydalı bulduysanız, bir yedek satın alıp bu proje üzerinde çalışmaya devam edebilmem için GitHub Sponsors\'ta beni desteklemeyi düşünün - küçük bir miktar bile çok işe yarar. Desteğiniz için teşekkürler!</string>
|
|
||||||
<string name="support_librepods">LibrePods\'u Destekle</string>
|
|
||||||
<string name="listening_mode_off_description">Gürültü yönetimini kapatır</string>
|
<string name="listening_mode_off_description">Gürültü yönetimini kapatır</string>
|
||||||
<string name="listening_mode_transparency_description">Dış sesleri içeri alır</string>
|
<string name="listening_mode_transparency_description">Dış sesleri içeri alır</string>
|
||||||
<string name="listening_mode_adaptive_description">Dış gürültüyü dinamik olarak ayarlar</string>
|
<string name="listening_mode_adaptive_description">Dış gürültüyü dinamik olarak ayarlar</string>
|
||||||
|
|||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Навколишній Шум</string>
|
<string name="environmental_noise">Навколишній Шум</string>
|
||||||
<string name="reconnect_to_last_device">Перепідключитися до останнього підключеного пристрою</string>
|
<string name="reconnect_to_last_device">Перепідключитися до останнього підключеного пристрою</string>
|
||||||
<string name="disconnect">Відʼєднатися</string>
|
<string name="disconnect">Відʼєднатися</string>
|
||||||
<string name="support_me">Підтримати мене</string>
|
|
||||||
<string name="never_show_again">Ніколи не показувати знову</string>
|
|
||||||
<string name="support_dialog_description">Нещодавно я втратив свій лівий AirPod. Якщо LibrePods виявилися корисними для вас, розгляньте можливість підтримати мене на GitHub Sponsors, щоб я міг купити заміну та продовжити роботу над цим проектом — навіть невелика допомога має велике значення. Дякую за вашу підтримку!</string>
|
|
||||||
<string name="support_librepods">Підтримати LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Вимикає керування шумом</string>
|
<string name="listening_mode_off_description">Вимикає керування шумом</string>
|
||||||
<string name="listening_mode_transparency_description">Пропускає зовнішні звуки</string>
|
<string name="listening_mode_transparency_description">Пропускає зовнішні звуки</string>
|
||||||
<string name="listening_mode_adaptive_description">Динамічно налаштовує зовнішній шум</string>
|
<string name="listening_mode_adaptive_description">Динамічно налаштовує зовнішній шум</string>
|
||||||
|
|||||||
@@ -206,10 +206,6 @@
|
|||||||
<string name="environmental_noise">Tiếng ồn môi trường</string>
|
<string name="environmental_noise">Tiếng ồn môi trường</string>
|
||||||
<string name="reconnect_to_last_device">Kết nối lại với thiết bị được kết nối lần cuối</string>
|
<string name="reconnect_to_last_device">Kết nối lại với thiết bị được kết nối lần cuối</string>
|
||||||
<string name="disconnect">Ngắt kết nối</string>
|
<string name="disconnect">Ngắt kết nối</string>
|
||||||
<string name="support_me">Hỗ trợ tôi</string>
|
|
||||||
<string name="never_show_again">Không hiển thị lại</string>
|
|
||||||
<string name="support_dialog_description">Gần đây tôi bị mất tai bên trái của AirPod. Nếu bạn thấy LibrePods hữu ích, hãy cân nhắc hỗ trợ tôi trên GitHub Sponsors để tôi có thể mua cái thay thế và tiếp tục làm việc trên dự án này - ngay cả một khoản nhỏ cũng rất có ý nghĩa. Cảm ơn sự hỗ trợ của bạn!</string>
|
|
||||||
<string name="support_librepods">Hỗ trợ LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Tắt quản lý tiếng ồn</string>
|
<string name="listening_mode_off_description">Tắt quản lý tiếng ồn</string>
|
||||||
<string name="listening_mode_transparency_description">Cho phép âm thanh bên ngoài</string>
|
<string name="listening_mode_transparency_description">Cho phép âm thanh bên ngoài</string>
|
||||||
<string name="listening_mode_adaptive_description">Điều chỉnh động tiếng ồn bên ngoài</string>
|
<string name="listening_mode_adaptive_description">Điều chỉnh động tiếng ồn bên ngoài</string>
|
||||||
|
|||||||
@@ -207,10 +207,6 @@
|
|||||||
<string name="environmental_noise">环境噪音</string>
|
<string name="environmental_noise">环境噪音</string>
|
||||||
<string name="reconnect_to_last_device">重新连接到上次连接的设备</string>
|
<string name="reconnect_to_last_device">重新连接到上次连接的设备</string>
|
||||||
<string name="disconnect">断开连接</string>
|
<string name="disconnect">断开连接</string>
|
||||||
<string name="support_me">支持我</string>
|
|
||||||
<string name="never_show_again">不再显示</string>
|
|
||||||
<string name="support_dialog_description">我最近丢了我的左耳 AirPod。如果你觉得 LibrePods 有用,请考虑在 GitHub Sponsors 上支持我,这样我就可以购买一个替换品并继续从事这个项目——即使是少量捐助也能发挥很大作用。感谢你的支持!</string>
|
|
||||||
<string name="support_librepods">支持 LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">关闭噪音管理</string>
|
<string name="listening_mode_off_description">关闭噪音管理</string>
|
||||||
<string name="listening_mode_transparency_description">允许外部声音进入</string>
|
<string name="listening_mode_transparency_description">允许外部声音进入</string>
|
||||||
<string name="listening_mode_adaptive_description">动态调整外部噪音</string>
|
<string name="listening_mode_adaptive_description">动态调整外部噪音</string>
|
||||||
|
|||||||
@@ -208,10 +208,6 @@
|
|||||||
<string name="environmental_noise">環境噪音</string>
|
<string name="environmental_noise">環境噪音</string>
|
||||||
<string name="reconnect_to_last_device">重新連接至上次連接的裝置</string>
|
<string name="reconnect_to_last_device">重新連接至上次連接的裝置</string>
|
||||||
<string name="disconnect">中斷連線</string>
|
<string name="disconnect">中斷連線</string>
|
||||||
<string name="support_me">贊助我</string>
|
|
||||||
<string name="never_show_again">不再顯示</string>
|
|
||||||
<string name="support_dialog_description">我最近弄丟了左耳的 AirPod。如果你覺得 LibrePods 很好用,請考慮在 GitHub Sponsors 上贊助我,讓我能買個替換品並繼續開發這個專案,一點點金額也能帶來很大的幫助。感謝你的支持!</string>
|
|
||||||
<string name="support_librepods">贊助 LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">關閉噪音管理</string>
|
<string name="listening_mode_off_description">關閉噪音管理</string>
|
||||||
<string name="listening_mode_transparency_description">允許外部聲音</string>
|
<string name="listening_mode_transparency_description">允許外部聲音</string>
|
||||||
<string name="listening_mode_adaptive_description">動態調整外部噪音</string>
|
<string name="listening_mode_adaptive_description">動態調整外部噪音</string>
|
||||||
|
|||||||
@@ -206,12 +206,9 @@
|
|||||||
<string name="environmental_noise">Environmental Noise</string>
|
<string name="environmental_noise">Environmental Noise</string>
|
||||||
<string name="reconnect_to_last_device">Reconnect to last connected device</string>
|
<string name="reconnect_to_last_device">Reconnect to last connected device</string>
|
||||||
<string name="disconnect">Disconnect</string>
|
<string name="disconnect">Disconnect</string>
|
||||||
<string name="support_me">Support me</string>
|
|
||||||
<string name="never_show_again">Never show again</string>
|
|
||||||
<string name="support_dialog_description">I recently lost my left AirPod. If you\'ve found LibrePods useful, consider supporting me on GitHub Sponsors so I can buy a replacement and continue working on this project- even a little amount goes a long way. Thank you for your support!</string>
|
|
||||||
<string name="support_librepods">Support LibrePods</string>
|
|
||||||
<string name="listening_mode_off_description">Turns off noise management</string>
|
<string name="listening_mode_off_description">Turns off noise management</string>
|
||||||
<string name="listening_mode_transparency_description">Lets in external sounds</string>
|
<string name="listening_mode_transparency_description">Lets in external sounds</string>
|
||||||
<string name="listening_mode_adaptive_description">Dynamically adjust external noise</string>
|
<string name="listening_mode_adaptive_description">Dynamically adjust external noise</string>
|
||||||
<string name="listening_mode_noise_cancellation_description">Blocks out external sounds</string>
|
<string name="listening_mode_noise_cancellation_description">Blocks out external sounds</string>
|
||||||
|
<string name="unlock_all_features">Unlock all features</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
alias(libs.plugins.aboutLibraries) apply false
|
alias(libs.plugins.aboutLibraries) apply false
|
||||||
|
// alias(libs.plugins.hilt) apply false
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx8192m -Dfile.encoding=UTF-8
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. For more details, visit
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
@@ -23,3 +23,16 @@ kotlin.code.style=official
|
|||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
|
||||||
android.javaCompile.suppressSourceTargetDeprecationWarning=true
|
android.javaCompile.suppressSourceTargetDeprecationWarning=true
|
||||||
|
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
#android.defaults.buildfeatures.resvalues=true
|
||||||
|
#android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||||
|
#android.enableAppCompileTimeRClass=false
|
||||||
|
#android.usesSdkInManifest.disallowed=false
|
||||||
|
#android.uniquePackageNames=false
|
||||||
|
#android.dependency.useConstraints=true
|
||||||
|
#android.r8.strictFullModeForKeepRules=false
|
||||||
|
#android.r8.optimizedResourceShrinking=false
|
||||||
|
#android.builtInKotlin=false
|
||||||
|
#android.newDsl=false
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanistPermissions = "0.36.0"
|
accompanistPermissions = "0.37.3"
|
||||||
agp = "8.9.1"
|
agp = "9.1.0"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.3.20"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.18.0"
|
||||||
lifecycleRuntimeKtx = "2.8.7"
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
activityCompose = "1.10.1"
|
activityCompose = "1.13.0"
|
||||||
composeBom = "2025.04.00"
|
composeBom = "2026.03.01"
|
||||||
annotations = "26.0.2"
|
annotations = "26.1.0"
|
||||||
navigationCompose = "2.8.9"
|
navigationCompose = "2.9.7"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
haze = "1.6.10"
|
haze = "1.7.2"
|
||||||
hazeMaterials = "1.6.10"
|
hazeMaterials = "1.7.2"
|
||||||
dynamicanimation = "1.1.0"
|
dynamicanimation = "1.1.0"
|
||||||
foundationLayout = "1.9.1"
|
aboutLibraries = "14.0.1"
|
||||||
uiTooling = "1.9.1"
|
materialIconsCore = "1.7.8"
|
||||||
ui = "1.9.2"
|
backdrop = "2.0.0-alpha03"
|
||||||
aboutLibraries = "13.0.0-rc01"
|
billing = "8.3.0"
|
||||||
|
hilt = "2.59.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
@@ -33,14 +34,19 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
|
|||||||
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
|
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
|
||||||
haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "hazeMaterials" }
|
haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "hazeMaterials" }
|
||||||
androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynamicanimation", version.ref = "dynamicanimation" }
|
androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynamicanimation", version.ref = "dynamicanimation" }
|
||||||
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
|
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout"}
|
||||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
aboutlibraries = { group = "com.mikepenz", name = "aboutlibraries", version.ref = "aboutLibraries" }
|
aboutlibraries = { group = "com.mikepenz", name = "aboutlibraries", version.ref = "aboutLibraries" }
|
||||||
aboutlibraries-compose-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
|
aboutlibraries-compose-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
|
||||||
|
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "materialIconsCore" }
|
||||||
|
backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backdrop" }
|
||||||
|
billing = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
|
||||||
|
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||||
|
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }
|
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }
|
||||||
|
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#Mon Oct 07 22:30:36 IST 2024
|
#Mon Oct 07 22:30:36 IST 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user