mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-07-08 15:14:57 +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
+4
-1
@@ -18,4 +18,7 @@
|
|||||||
|
|
||||||
# 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,9 +197,9 @@ 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()
|
|
||||||
}
|
|
||||||
|
|||||||
+62
-56
@@ -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
|
||||||
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.hearing_health),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
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
|
modifier = Modifier
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
.padding(horizontal = 12.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
)
|
||||||
){
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.hearing_health),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
+13
-19
@@ -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
|
||||||
@@ -181,4 +175,4 @@ fun StyledSelectList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}")
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-33
@@ -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),
|
||||||
@@ -189,4 +176,4 @@ fun VersionScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -61,4 +61,4 @@ fun LibrePodsTheme(
|
|||||||
typography = Typography,
|
typography = Typography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,217 +1,213 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
<string name="app_name" translatable="false">LibrePods</string>
|
<string name="app_name" translatable="false">LibrePods</string>
|
||||||
<string name="app_description">Libera i tuoi AirPods dall'ecosistema Apple.</string>
|
<string name="app_description">Libera i tuoi AirPods dall'ecosistema Apple.</string>
|
||||||
<string name="app_widget_description">Visualizza lo stato della batteria dei tuoi AirPods direttamente dalla schermata principale!</string>
|
<string name="app_widget_description">Visualizza lo stato della batteria dei tuoi AirPods direttamente dalla schermata principale!</string>
|
||||||
<string name="accessibility">Accessibilità</string>
|
<string name="accessibility">Accessibilità</string>
|
||||||
<string name="tone_volume">Volume Tono</string>
|
<string name="tone_volume">Volume Tono</string>
|
||||||
<string name="tone_volume_description">Regola il volume del tono degli effetti sonori riprodotti dagli AirPods.</string>
|
<string name="tone_volume_description">Regola il volume del tono degli effetti sonori riprodotti dagli AirPods.</string>
|
||||||
<string name="audio">Audio</string>
|
<string name="audio">Audio</string>
|
||||||
<string name="adaptive_audio">Audio Adattivo</string>
|
<string name="adaptive_audio">Audio Adattivo</string>
|
||||||
<string name="customize_adaptive_audio">Personalizza Audio Adattivo</string>
|
<string name="customize_adaptive_audio">Personalizza Audio Adattivo</string>
|
||||||
<string name="adaptive_audio_description">L'audio adattivo risponde dinamicamente al tuo ambiente e cancella o permette i rumori esterni. Puoi personalizzare l'Audio Adattivo per permettere più o meno rumore.</string>
|
<string name="adaptive_audio_description">L'audio adattivo risponde dinamicamente al tuo ambiente e cancella o permette i rumori esterni. Puoi personalizzare l'Audio Adattivo per permettere più o meno rumore.</string>
|
||||||
<string name="buds">Auricolari</string>
|
<string name="buds">Auricolari</string>
|
||||||
<string name="case_alt">Custodia</string>
|
<string name="case_alt">Custodia</string>
|
||||||
<string name="test">Test</string>
|
<string name="test">Test</string>
|
||||||
<string name="name">Nome</string>
|
<string name="name">Nome</string>
|
||||||
<string name="noise_control">Modalità di Ascolto</string>
|
<string name="noise_control">Modalità di Ascolto</string>
|
||||||
<string name="off">Spento</string>
|
<string name="off">Spento</string>
|
||||||
<string name="transparency">Trasparenza</string>
|
<string name="transparency">Trasparenza</string>
|
||||||
<string name="adaptive">Adattivo</string>
|
<string name="adaptive">Adattivo</string>
|
||||||
<string name="noise_cancellation">Cancellazione del Rumore</string>
|
<string name="noise_cancellation">Cancellazione del Rumore</string>
|
||||||
<string name="press_and_hold_airpods">Premi e Tieni Premuto sugli AirPods</string>
|
<string name="press_and_hold_airpods">Premi e Tieni Premuto sugli AirPods</string>
|
||||||
<string name="press_and_hold_noise_control_description">Premi e tieni premuto sullo stelo per alternare tra le modalità di ascolto selezionate.</string>
|
<string name="press_and_hold_noise_control_description">Premi e tieni premuto sullo stelo per alternare tra le modalità di ascolto selezionate.</string>
|
||||||
<string name="head_gestures">Gesti della Testa</string>
|
<string name="head_gestures">Gesti della Testa</string>
|
||||||
<string name="left">Sinistra</string>
|
<string name="left">Sinistra</string>
|
||||||
<string name="right">Destra</string>
|
<string name="right">Destra</string>
|
||||||
<string name="conversational_awareness">Consapevolezza Conversazionale</string>
|
<string name="conversational_awareness">Consapevolezza Conversazionale</string>
|
||||||
<string name="conversational_awareness_description">Abbassa il volume dei contenuti multimediali e riduce il rumore di fondo quando inizi a parlare con altre persone.</string>
|
<string name="conversational_awareness_description">Abbassa il volume dei contenuti multimediali e riduce il rumore di fondo quando inizi a parlare con altre persone.</string>
|
||||||
<string name="personalized_volume">Volume Personalizzato</string>
|
<string name="personalized_volume">Volume Personalizzato</string>
|
||||||
<string name="personalized_volume_description">Regola il volume dei contenuti multimediali in risposta al tuo ambiente.</string>
|
<string name="personalized_volume_description">Regola il volume dei contenuti multimediali in risposta al tuo ambiente.</string>
|
||||||
<string name="noise_cancellation_single_airpod">Cancellazione del Rumore con un Solo AirPod</string>
|
<string name="noise_cancellation_single_airpod">Cancellazione del Rumore con un Solo AirPod</string>
|
||||||
<string name="noise_cancellation_single_airpod_description">Consenti agli AirPods di essere messi in modalità di cancellazione del rumore quando è presente un solo AirPod nell'orecchio.</string>
|
<string name="noise_cancellation_single_airpod_description">Consenti agli AirPods di essere messi in modalità di cancellazione del rumore quando è presente un solo AirPod nell'orecchio.</string>
|
||||||
<string name="volume_control">Controllo Volume</string>
|
<string name="volume_control">Controllo Volume</string>
|
||||||
<string name="volume_control_description">Regola il volume scorrendo verso l'alto o verso il basso sul sensore situato sullo stelo degli AirPods Pro.</string>
|
<string name="volume_control_description">Regola il volume scorrendo verso l'alto o verso il basso sul sensore situato sullo stelo degli AirPods Pro.</string>
|
||||||
<string name="airpods_not_connected">AirPods non connessi</string>
|
<string name="airpods_not_connected">AirPods non connessi</string>
|
||||||
<string name="airpods_not_connected_description">Si prega di connettere i tuoi AirPods per accedere alle impostazioni.</string>
|
<string name="airpods_not_connected_description">Si prega di connettere i tuoi AirPods per accedere alle impostazioni.</string>
|
||||||
<string name="back">Indietro</string>
|
<string name="back">Indietro</string>
|
||||||
<string name="app_settings">Personalizzazioni</string>
|
<string name="app_settings">Personalizzazioni</string>
|
||||||
<string name="relative_conversational_awareness_volume">Volume relativo</string>
|
<string name="relative_conversational_awareness_volume">Volume relativo</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Riduce a una percentuale del volume corrente invece del volume massimo.</string>
|
<string name="relative_conversational_awareness_volume_description">Riduce a una percentuale del volume corrente invece del volume massimo.</string>
|
||||||
<string name="conversational_awareness_pause_music">Metti in Pausa la Musica</string>
|
<string name="conversational_awareness_pause_music">Metti in Pausa la Musica</string>
|
||||||
<string name="conversational_awareness_pause_music_description">Quando inizi a parlare, la musica verrà messa in pausa.</string>
|
<string name="conversational_awareness_pause_music_description">Quando inizi a parlare, la musica verrà messa in pausa.</string>
|
||||||
<string name="appwidget_text">ESEMPIO</string>
|
<string name="appwidget_text">ESEMPIO</string>
|
||||||
<string name="add_widget">Aggiungi widget</string>
|
<string name="add_widget">Aggiungi widget</string>
|
||||||
<string name="noise_control_widget_description">Controlla la Modalità di Controllo del Rumore direttamente dalla tua Schermata Principale.</string>
|
<string name="noise_control_widget_description">Controlla la Modalità di Controllo del Rumore direttamente dalla tua Schermata Principale.</string>
|
||||||
<string name="island_connected_text">Connesso</string>
|
<string name="island_connected_text">Connesso</string>
|
||||||
<string name="island_connected_remote_text">Connesso a Linux</string>
|
<string name="island_connected_remote_text">Connesso a Linux</string>
|
||||||
<string name="island_taking_over_text">Connesso</string>
|
<string name="island_taking_over_text">Connesso</string>
|
||||||
<string name="island_moved_to_remote_text">Spostato su Linux</string>
|
<string name="island_moved_to_remote_text">Spostato su Linux</string>
|
||||||
<string name="island_moved_to_other_device_text">Spostato su %1$s</string>
|
<string name="island_moved_to_other_device_text">Spostato su %1$s</string>
|
||||||
<string name="island_moved_to_other_device_reversed_text">Riconnetti dalla notifica</string>
|
<string name="island_moved_to_other_device_reversed_text">Riconnetti dalla notifica</string>
|
||||||
<string name="head_tracking">Tracciamento della Testa</string>
|
<string name="head_tracking">Tracciamento della Testa</string>
|
||||||
<string name="head_gestures_details">Annuisci per rispondere alle chiamate e scuoti la testa per rifiutarle.</string>
|
<string name="head_gestures_details">Annuisci per rispondere alle chiamate e scuoti la testa per rifiutarle.</string>
|
||||||
<string name="general_settings_header">Generale</string>
|
<string name="general_settings_header">Generale</string>
|
||||||
<string name="qs_click_behavior_title">Azione del Tile Impostazioni Rapide</string>
|
<string name="qs_click_behavior_title">Azione del Tile Impostazioni Rapide</string>
|
||||||
<string name="qs_click_behavior_dialog_desc">Mostra la finestra di dialogo per il controllo del rumore al tocco.</string>
|
<string name="qs_click_behavior_dialog_desc">Mostra la finestra di dialogo per il controllo del rumore al tocco.</string>
|
||||||
<string name="qs_click_behavior_cycle_desc">Alterna tra le modalità al tocco.</string>
|
<string name="qs_click_behavior_cycle_desc">Alterna tra le modalità al tocco.</string>
|
||||||
<string name="developer_options_header">Sviluppatore</string>
|
<string name="developer_options_header">Sviluppatore</string>
|
||||||
<string name="more_settings_title">Apri le Impostazioni degli AirPods</string>
|
<string name="more_settings_title">Apri le Impostazioni degli AirPods</string>
|
||||||
<string name="more_settings_subtitle">Gestisci le funzionalità e le preferenze degli AirPods</string>
|
<string name="more_settings_subtitle">Gestisci le funzionalità e le preferenze degli AirPods</string>
|
||||||
<string name="ear_detection">Rilevamento Automatico dell'Orecchio</string>
|
<string name="ear_detection">Rilevamento Automatico dell'Orecchio</string>
|
||||||
<string name="auto_play">Riproduzione Automatica</string>
|
<string name="auto_play">Riproduzione Automatica</string>
|
||||||
<string name="auto_pause">Pausa Automatica</string>
|
<string name="auto_pause">Pausa Automatica</string>
|
||||||
<string name="troubleshooting">Risoluzione dei Problemi</string>
|
<string name="troubleshooting">Risoluzione dei Problemi</string>
|
||||||
<string name="troubleshooting_description">Raccogli i log per diagnosticare i problemi con la connessione degli AirPods</string>
|
<string name="troubleshooting_description">Raccogli i log per diagnosticare i problemi con la connessione degli AirPods</string>
|
||||||
<string name="collect_logs">Raccogli Log</string>
|
<string name="collect_logs">Raccogli Log</string>
|
||||||
<string name="saved_logs">Log Salvati</string>
|
<string name="saved_logs">Log Salvati</string>
|
||||||
<string name="no_logs_found">Nessun log salvato trovato</string>
|
<string name="no_logs_found">Nessun log salvato trovato</string>
|
||||||
<string name="takeover_header">Preferenze di Connessione Automatica</string>
|
<string name="takeover_header">Preferenze di Connessione Automatica</string>
|
||||||
<string name="takeover_airpods_state">Connetti ai tuoi AirPods quando il loro stato è:</string>
|
<string name="takeover_airpods_state">Connetti ai tuoi AirPods quando il loro stato è:</string>
|
||||||
<string name="takeover_disconnected">Disconnesso</string>
|
<string name="takeover_disconnected">Disconnesso</string>
|
||||||
<string name="takeover_disconnected_desc">Gli AirPods non sono connessi a un dispositivo</string>
|
<string name="takeover_disconnected_desc">Gli AirPods non sono connessi a un dispositivo</string>
|
||||||
<string name="takeover_idle">Inattivo</string>
|
<string name="takeover_idle">Inattivo</string>
|
||||||
<string name="takeover_idle_desc">Un dispositivo è connesso ai tuoi AirPods, ma non riproduce contenuti multimediali né è in chiamata</string>
|
<string name="takeover_idle_desc">Un dispositivo è connesso ai tuoi AirPods, ma non riproduce contenuti multimediali né è in chiamata</string>
|
||||||
<string name="takeover_music">Riproduzione di contenuti multimediali</string>
|
<string name="takeover_music">Riproduzione di contenuti multimediali</string>
|
||||||
<string name="takeover_music_desc">Un dispositivo sta riproducendo contenuti multimediali sui tuoi AirPods</string>
|
<string name="takeover_music_desc">Un dispositivo sta riproducendo contenuti multimediali sui tuoi AirPods</string>
|
||||||
<string name="takeover_call">In chiamata</string>
|
<string name="takeover_call">In chiamata</string>
|
||||||
<string name="takeover_call_desc">Un dispositivo è in chiamata con i tuoi AirPods</string>
|
<string name="takeover_call_desc">Un dispositivo è in chiamata con i tuoi AirPods</string>
|
||||||
<string name="takeover_phone_state">Connetti agli AirPods quando il tuo telefono è:</string>
|
<string name="takeover_phone_state">Connetti agli AirPods quando il tuo telefono è:</string>
|
||||||
<string name="takeover_ringing_call">Ricezione di una chiamata</string>
|
<string name="takeover_ringing_call">Ricezione di una chiamata</string>
|
||||||
<string name="takeover_ringing_call_desc">Il tuo telefono inizia a squillare</string>
|
<string name="takeover_ringing_call_desc">Il tuo telefono inizia a squillare</string>
|
||||||
<string name="takeover_media_start">Avvio della riproduzione di contenuti multimediali</string>
|
<string name="takeover_media_start">Avvio della riproduzione di contenuti multimediali</string>
|
||||||
<string name="takeover_media_start_desc">Il tuo telefono inizia a riprodurre contenuti multimediali</string>
|
<string name="takeover_media_start_desc">Il tuo telefono inizia a riprodurre contenuti multimediali</string>
|
||||||
<string name="undo">Annulla</string>
|
<string name="undo">Annulla</string>
|
||||||
<string name="customize_transparency_mode_description">Puoi personalizzare la modalità Trasparenza per i tuoi AirPods Pro per aiutarti a sentire ciò che ti circonda.</string>
|
<string name="customize_transparency_mode_description">Puoi personalizzare la modalità Trasparenza per i tuoi AirPods Pro per aiutarti a sentire ciò che ti circonda.</string>
|
||||||
<string name="loud_sound_reduction_description">La Riduzione dei Suoni Forti può ridurre attivamente la tua esposizione ai forti rumori ambientali quando in modalità Trasparenza e Adattiva. La Riduzione dei Suoni Forti non è attiva in modalità Spento.</string>
|
<string name="loud_sound_reduction_description">La Riduzione dei Suoni Forti può ridurre attivamente la tua esposizione ai forti rumori ambientali quando in modalità Trasparenza e Adattiva. La Riduzione dei Suoni Forti non è attiva in modalità Spento.</string>
|
||||||
<string name="loud_sound_reduction">Riduzione dei Suoni Forti</string>
|
<string name="loud_sound_reduction">Riduzione dei Suoni Forti</string>
|
||||||
<string name="call_controls">Controlli Chiamata</string>
|
<string name="call_controls">Controlli Chiamata</string>
|
||||||
<string name="automatically_connect">Connetti automaticamente a questo dispositivo</string>
|
<string name="automatically_connect">Connetti automaticamente a questo dispositivo</string>
|
||||||
<string name="automatically_connect_description">Quando abilitato, gli AirPods tenteranno di connettersi automaticamente a questo dispositivo. Altrimenti, tenteranno di connettersi automaticamente solo se sono stati connessi in precedenza.</string>
|
<string name="automatically_connect_description">Quando abilitato, gli AirPods tenteranno di connettersi automaticamente a questo dispositivo. Altrimenti, tenteranno di connettersi automaticamente solo se sono stati connessi in precedenza.</string>
|
||||||
<string name="sleep_detection">Metti in pausa i contenuti multimediali quando ti addormenti</string>
|
<string name="sleep_detection">Metti in pausa i contenuti multimediali quando ti addormenti</string>
|
||||||
<string name="off_listening_mode">Modalità Ascolto Disattivata</string>
|
<string name="off_listening_mode">Modalità Ascolto Disattivata</string>
|
||||||
<string name="off_listening_mode_description">Quando questa opzione è attiva, le modalità di ascolto degli AirPods includeranno un'opzione "Spento". I livelli di suono forti non vengono ridotti quando la modalità di ascolto è impostata su "Spento".</string>
|
<string name="off_listening_mode_description">Quando questa opzione è attiva, le modalità di ascolto degli AirPods includeranno un'opzione "Spento". I livelli di suono forti non vengono ridotti quando la modalità di ascolto è impostata su "Spento".</string>
|
||||||
<string name="microphone">Microfono</string>
|
<string name="microphone">Microfono</string>
|
||||||
<string name="microphone_mode">Modalità Microfono</string>
|
<string name="microphone_mode">Modalità Microfono</string>
|
||||||
<string name="microphone_automatic">Automatico</string>
|
<string name="microphone_automatic">Automatico</string>
|
||||||
<string name="microphone_always_right">Sempre Destro</string>
|
<string name="microphone_always_right">Sempre Destro</string>
|
||||||
<string name="microphone_always_left">Sempre Sinistro</string>
|
<string name="microphone_always_left">Sempre Sinistro</string>
|
||||||
<string name="answer_call">Rispondi alla chiamata</string>
|
<string name="answer_call">Rispondi alla chiamata</string>
|
||||||
<string name="mute_unmute">Silenzia/Riattiva</string>
|
<string name="mute_unmute">Silenzia/Riattiva</string>
|
||||||
<string name="hang_up">Riaggancia</string>
|
<string name="hang_up">Riaggancia</string>
|
||||||
<string name="press_once">Premi una Volta</string>
|
<string name="press_once">Premi una Volta</string>
|
||||||
<string name="press_twice">Premi Due Volte</string>
|
<string name="press_twice">Premi Due Volte</string>
|
||||||
<string name="hearing_aid">Apparecchio Acustico</string>
|
<string name="hearing_aid">Apparecchio Acustico</string>
|
||||||
<string name="adjustments">Regolazioni</string>
|
<string name="adjustments">Regolazioni</string>
|
||||||
<string name="swipe_to_control_amplification">Scorri per controllare l'amplificazione</string>
|
<string name="swipe_to_control_amplification">Scorri per controllare l'amplificazione</string>
|
||||||
<string name="swipe_amplification_description">Quando sei in modalità Trasparenza e nessun contenuto multimediale è in riproduzione, scorri verso l'alto e verso il basso sui controlli Touch dei tuoi AirPods Pro per aumentare o diminuire l'amplificazione dei suoni ambientali.</string>
|
<string name="swipe_amplification_description">Quando sei in modalità Trasparenza e nessun contenuto multimediale è in riproduzione, scorri verso l'alto e verso il basso sui controlli Touch dei tuoi AirPods Pro per aumentare o diminuire l'amplificazione dei suoni ambientali.</string>
|
||||||
<string name="transparency_mode">Modalità Trasparenza</string>
|
<string name="transparency_mode">Modalità Trasparenza</string>
|
||||||
<string name="customize_transparency_mode">Personalizza la Modalità Trasparenza</string>
|
<string name="customize_transparency_mode">Personalizza la Modalità Trasparenza</string>
|
||||||
<string name="press_speed">Velocità di Pressione</string>
|
<string name="press_speed">Velocità di Pressione</string>
|
||||||
<string name="press_speed_description">Regola la velocità richiesta per premere due o tre volte sui tuoi AirPods.</string>
|
<string name="press_speed_description">Regola la velocità richiesta per premere due o tre volte sui tuoi AirPods.</string>
|
||||||
<string name="press_and_hold_duration">Durata della Pressione Prolungata</string>
|
<string name="press_and_hold_duration">Durata della Pressione Prolungata</string>
|
||||||
<string name="press_and_hold_duration_description">Regola la durata richiesta per premere e tenere premuto sui tuoi AirPods.</string>
|
<string name="press_and_hold_duration_description">Regola la durata richiesta per premere e tenere premuto sui tuoi AirPods.</string>
|
||||||
<string name="volume_swipe_speed">Velocità di Scorrimento del Volume</string>
|
<string name="volume_swipe_speed">Velocità di Scorrimento del Volume</string>
|
||||||
<string name="volume_swipe_speed_description">Per evitare regolazioni involontarie del volume, seleziona il tempo di attesa preferito tra gli scorrimenti.</string>
|
<string name="volume_swipe_speed_description">Per evitare regolazioni involontarie del volume, seleziona il tempo di attesa preferito tra gli scorrimenti.</string>
|
||||||
<string name="equalizer">Equalizzatore</string>
|
<string name="equalizer">Equalizzatore</string>
|
||||||
<string name="apply_eq_to">Applica EQ a</string>
|
<string name="apply_eq_to">Applica EQ a</string>
|
||||||
<string name="phone">Telefono</string>
|
<string name="phone">Telefono</string>
|
||||||
<string name="media">Media</string>
|
<string name="media">Media</string>
|
||||||
<string name="band_label">Banda %d</string>
|
<string name="band_label">Banda %d</string>
|
||||||
<string name="default_option">Predefinito</string>
|
<string name="default_option">Predefinito</string>
|
||||||
<string name="slower">Più lento</string>
|
<string name="slower">Più lento</string>
|
||||||
<string name="slowest">Il più lento</string>
|
<string name="slowest">Il più lento</string>
|
||||||
<string name="longer">Più lungo</string>
|
<string name="longer">Più lungo</string>
|
||||||
<string name="longest">Il più lungo</string>
|
<string name="longest">Il più lungo</string>
|
||||||
<string name="darker">Più scuro</string>
|
<string name="darker">Più scuro</string>
|
||||||
<string name="brighter">Più luminoso</string>
|
<string name="brighter">Più luminoso</string>
|
||||||
<string name="less">Meno</string>
|
<string name="less">Meno</string>
|
||||||
<string name="more">Di più</string>
|
<string name="more">Di più</string>
|
||||||
<string name="amplification">Amplificazione</string>
|
<string name="amplification">Amplificazione</string>
|
||||||
<string name="balance">Bilanciamento</string>
|
<string name="balance">Bilanciamento</string>
|
||||||
<string name="tone">Tono</string>
|
<string name="tone">Tono</string>
|
||||||
<string name="ambient_noise_reduction">Riduzione del Rumore Ambientale</string>
|
<string name="ambient_noise_reduction">Riduzione del Rumore Ambientale</string>
|
||||||
<string name="conversation_boost">Potenziamento Conversazione</string>
|
<string name="conversation_boost">Potenziamento Conversazione</string>
|
||||||
<string name="conversation_boost_description">Potenziamento Conversazione concentra i tuoi AirPods Pro sulla persona che parla di fronte a te, rendendo più facile sentire in una conversazione faccia a faccia.</string>
|
<string name="conversation_boost_description">Potenziamento Conversazione concentra i tuoi AirPods Pro sulla persona che parla di fronte a te, rendendo più facile sentire in una conversazione faccia a faccia.</string>
|
||||||
<string name="hearing_aid_description">Gli AirPods possono utilizzare i risultati di un test dell'udito per apportare modifiche che migliorano la chiarezza delle voci e dei suoni intorno a te.\n\nApparecchio Acustico è destinato solo a persone con perdita dell'udito da lieve a moderata.</string>
|
<string name="hearing_aid_description">Gli AirPods possono utilizzare i risultati di un test dell'udito per apportare modifiche che migliorano la chiarezza delle voci e dei suoni intorno a te.\n\nApparecchio Acustico è destinato solo a persone con perdita dell'udito da lieve a moderata.</string>
|
||||||
<string name="media_assist">Assistenza Media</string>
|
<string name="media_assist">Assistenza Media</string>
|
||||||
<string name="media_assist_description">Gli AirPods Pro possono utilizzare i risultati di un test dell'udito per apportare modifiche che migliorano la chiarezza di musica, video e chiamate.</string>
|
<string name="media_assist_description">Gli AirPods Pro possono utilizzare i risultati di un test dell'udito per apportare modifiche che migliorano la chiarezza di musica, video e chiamate.</string>
|
||||||
<string name="adjust_media">Regola Musica e Video</string>
|
<string name="adjust_media">Regola Musica e Video</string>
|
||||||
<string name="adjust_calls">Regola Chiamate</string>
|
<string name="adjust_calls">Regola Chiamate</string>
|
||||||
<string name="widget">Widget</string>
|
<string name="widget">Widget</string>
|
||||||
<string name="show_phone_battery_in_widget">Mostra la batteria del telefono nel widget</string>
|
<string name="show_phone_battery_in_widget">Mostra la batteria del telefono nel widget</string>
|
||||||
<string name="show_phone_battery_in_widget_description">Visualizza il livello della batteria del tuo telefono nel widget accanto alla batteria degli AirPods</string>
|
<string name="show_phone_battery_in_widget_description">Visualizza il livello della batteria del tuo telefono nel widget accanto alla batteria degli AirPods</string>
|
||||||
<string name="conversational_awareness_volume">Volume Consapevolezza Conversazionale</string>
|
<string name="conversational_awareness_volume">Volume Consapevolezza Conversazionale</string>
|
||||||
<string name="quick_settings_tile">Tile Impostazioni Rapide</string>
|
<string name="quick_settings_tile">Tile Impostazioni Rapide</string>
|
||||||
<string name="open_dialog_for_controlling">Apri finestra di dialogo per il controllo</string>
|
<string name="open_dialog_for_controlling">Apri finestra di dialogo per il controllo</string>
|
||||||
<string name="open_dialog_for_controlling_description">Se disabilitato, cliccando sul QS si scorrerà tra le modalità. Se abilitato, verrà mostrata una finestra di dialogo per controllare la modalità di controllo del rumore e la consapevolezza conversazionale.</string>
|
<string name="open_dialog_for_controlling_description">Se disabilitato, cliccando sul QS si scorrerà tra le modalità. Se abilitato, verrà mostrata una finestra di dialogo per controllare la modalità di controllo del rumore e la consapevolezza conversazionale.</string>
|
||||||
<string name="disconnect_when_not_wearing">Disconnetti AirPods quando non indossati</string>
|
<string name="disconnect_when_not_wearing">Disconnetti AirPods quando non indossati</string>
|
||||||
<string name="disconnect_when_not_wearing_description">Sarai ancora in grado di controllarli con l'app - questo disconnette solo l'audio.</string>
|
<string name="disconnect_when_not_wearing_description">Sarai ancora in grado di controllarli con l'app - questo disconnette solo l'audio.</string>
|
||||||
<string name="advanced_options">Opzioni Avanzate</string>
|
<string name="advanced_options">Opzioni Avanzate</string>
|
||||||
<string name="set_identity_resolving_key">Imposta Chiave di Risoluzione Identità (IRK)</string>
|
<string name="set_identity_resolving_key">Imposta Chiave di Risoluzione Identità (IRK)</string>
|
||||||
<string name="set_identity_resolving_key_description">Imposta manualmente il valore IRK utilizzato per risolvere gli indirizzi casuali BLE</string>
|
<string name="set_identity_resolving_key_description">Imposta manualmente il valore IRK utilizzato per risolvere gli indirizzi casuali BLE</string>
|
||||||
<string name="set_encryption_key">Imposta Chiave di Crittografia</string>
|
<string name="set_encryption_key">Imposta Chiave di Crittografia</string>
|
||||||
<string name="set_encryption_key_description">Imposta manualmente il valore ENC_KEY utilizzato per decrittografare le pubblicità BLE</string>
|
<string name="set_encryption_key_description">Imposta manualmente il valore ENC_KEY utilizzato per decrittografare le pubblicità BLE</string>
|
||||||
<string name="use_alternate_head_tracking_packets">Utilizza pacchetti alternativi di tracciamento della testa</string>
|
<string name="use_alternate_head_tracking_packets">Utilizza pacchetti alternativi di tracciamento della testa</string>
|
||||||
<string name="use_alternate_head_tracking_packets_description">Abilita questo se il tracciamento della testa non funziona per te. Questo invia dati diversi agli AirPods per richiedere/interrompere i dati di tracciamento della testa.</string>
|
<string name="use_alternate_head_tracking_packets_description">Abilita questo se il tracciamento della testa non funziona per te. Questo invia dati diversi agli AirPods per richiedere/interrompere i dati di tracciamento della testa.</string>
|
||||||
<string name="act_as_an_apple_device">Comportati come un dispositivo Apple</string>
|
<string name="act_as_an_apple_device">Comportati come un dispositivo Apple</string>
|
||||||
<string name="act_as_an_apple_device_description">Abilita la connettività multi-dispositivo e le funzionalità di Accessibilità come la personalizzazione della modalità Trasparenza (amplificazione, tono, riduzione del rumore ambientale, potenziamento conversazione ed EQ)</string>
|
<string name="act_as_an_apple_device_description">Abilita la connettività multi-dispositivo e le funzionalità di Accessibilità come la personalizzazione della modalità Trasparenza (amplificazione, tono, riduzione del rumore ambientale, potenziamento conversazione ed EQ)</string>
|
||||||
<string name="act_as_an_apple_device_warning">Potrebbe essere instabile!! Un massimo di due dispositivi possono essere connessi ai tuoi AirPods. Se li stai usando con un dispositivo Apple come un iPad o un Mac, connetti prima quel dispositivo e poi il tuo Android.</string>
|
<string name="act_as_an_apple_device_warning">Potrebbe essere instabile!! Un massimo di due dispositivi possono essere connessi ai tuoi AirPods. Se li stai usando con un dispositivo Apple come un iPad o un Mac, connetti prima quel dispositivo e poi il tuo Android.</string>
|
||||||
<string name="reset_hook_offset">Reimposta Offset Hook</string>
|
<string name="reset_hook_offset">Reimposta Offset Hook</string>
|
||||||
<string name="reset_hook_offset_description">Questo cancellerà l'offset hook corrente e richiederà di rifare la procedura di configurazione. Sei sicuro di voler continuare?</string>
|
<string name="reset_hook_offset_description">Questo cancellerà l'offset hook corrente e richiederà di rifare la procedura di configurazione. Sei sicuro di voler continuare?</string>
|
||||||
<string name="reset">Reimposta</string>
|
<string name="reset">Reimposta</string>
|
||||||
<string name="hook_offset_reset_success">Offset hook è stato resettato. Reindirizzamento alla configurazione...</string>
|
<string name="hook_offset_reset_success">Offset hook è stato resettato. Reindirizzamento alla configurazione...</string>
|
||||||
<string name="hook_offset_reset_failure">Impossibile reimpostare l'offset hook</string>
|
<string name="hook_offset_reset_failure">Impossibile reimpostare l'offset hook</string>
|
||||||
<string name="irk_set_success">IRK impostata correttamente</string>
|
<string name="irk_set_success">IRK impostata correttamente</string>
|
||||||
<string name="encryption_key_set_success">Chiave di crittografia impostata correttamente</string>
|
<string name="encryption_key_set_success">Chiave di crittografia impostata correttamente</string>
|
||||||
<string name="irk_hex_value">Valore Esadecimale IRK</string>
|
<string name="irk_hex_value">Valore Esadecimale IRK</string>
|
||||||
<string name="enc_key_hex_value">Valore Esadecimale ENC_KEY</string>
|
<string name="enc_key_hex_value">Valore Esadecimale ENC_KEY</string>
|
||||||
<string name="enter_irk_hex">Inserisci IRK di 16 byte come stringa esadecimale (32 caratteri):</string>
|
<string name="enter_irk_hex">Inserisci IRK di 16 byte come stringa esadecimale (32 caratteri):</string>
|
||||||
<string name="enter_enc_key_hex">Inserisci ENC_KEY di 16 byte come stringa esadecimale (32 caratteri):</string>
|
<string name="enter_enc_key_hex">Inserisci ENC_KEY di 16 byte come stringa esadecimale (32 caratteri):</string>
|
||||||
<string name="must_be_32_hex_chars">Devono essere esattamente 32 caratteri esadecimali</string>
|
<string name="must_be_32_hex_chars">Devono essere esattamente 32 caratteri esadecimali</string>
|
||||||
<string name="error_converting_hex">Errore durante la conversione esadecimale:</string>
|
<string name="error_converting_hex">Errore durante la conversione esadecimale:</string>
|
||||||
<string name="found_offset_restart_bluetooth">Offset trovato, riavviare il processo Bluetooth</string>
|
<string name="found_offset_restart_bluetooth">Offset trovato, riavviare il processo Bluetooth</string>
|
||||||
<string name="digital_assistant">Assistente Digitale</string>
|
<string name="digital_assistant">Assistente Digitale</string>
|
||||||
<string name="on">Attivo</string>
|
<string name="on">Attivo</string>
|
||||||
<string name="camera_remote">Telecomando Fotocamera</string>
|
<string name="camera_remote">Telecomando Fotocamera</string>
|
||||||
<string name="camera_control">Controllo Fotocamera</string>
|
<string name="camera_control">Controllo Fotocamera</string>
|
||||||
<string name="camera_control_description">Scatta una foto, avvia o interrompi la registrazione e altro utilizzando Premere una Volta o Premere e Tenere Premuto. Quando si utilizzano gli AirPods per le azioni della fotocamera, se si seleziona Premere una Volta, i gesti di controllo dei media non saranno disponibili e, se si seleziona Premere e Tenere Premuto, la modalità di ascolto e i gesti dell'Assistente Digitale non saranno disponibili.</string>
|
<string name="camera_control_description">Scatta una foto, avvia o interrompi la registrazione e altro utilizzando Premere una Volta o Premere e Tenere Premuto. Quando si utilizzano gli AirPods per le azioni della fotocamera, se si seleziona Premere una Volta, i gesti di controllo dei media non saranno disponibili e, se si seleziona Premere e Tenere Premuto, la modalità di ascolto e i gesti dell'Assistente Digitale non saranno disponibili.</string>
|
||||||
<string name="camera_control_app_description">Imposta un pacchetto app personalizzato per il rilevamento della fotocamera</string>
|
<string name="camera_control_app_description">Imposta un pacchetto app personalizzato per il rilevamento della fotocamera</string>
|
||||||
<string name="set_custom_camera_package">Imposta Appid Fotocamera Personalizzata</string>
|
<string name="set_custom_camera_package">Imposta Appid Fotocamera Personalizzata</string>
|
||||||
<string name="enter_custom_camera_package">Inserisci l'id dell'applicazione della fotocamera:</string>
|
<string name="enter_custom_camera_package">Inserisci l'id dell'applicazione della fotocamera:</string>
|
||||||
<string name="custom_camera_package">Appid Fotocamera Personalizzata</string>
|
<string name="custom_camera_package">Appid Fotocamera Personalizzata</string>
|
||||||
<string name="custom_camera_package_set_success">Appid fotocamera personalizzata impostata correttamente</string>
|
<string name="custom_camera_package_set_success">Appid fotocamera personalizzata impostata correttamente</string>
|
||||||
<string name="app_listener_service_label">Ascoltatore fotocamera</string>
|
<string name="app_listener_service_label">Ascoltatore fotocamera</string>
|
||||||
<string name="app_listener_service_description">Servizio di ascolto per LibrePods per rilevare quando la fotocamera è attiva per attivare il controllo della fotocamera sugli AirPods.</string>
|
<string name="app_listener_service_description">Servizio di ascolto per LibrePods per rilevare quando la fotocamera è attiva per attivare il controllo della fotocamera sugli AirPods.</string>
|
||||||
<string name="open_source_licenses">Licenze Open Source</string>
|
<string name="open_source_licenses">Licenze Open Source</string>
|
||||||
<string name="hearing_test">Aggiorna Test Uditivo</string>
|
<string name="hearing_test">Aggiorna Test Uditivo</string>
|
||||||
<string name="update_hearing_test">Aggiorna Risultato Test Uditivo</string>
|
<string name="update_hearing_test">Aggiorna Risultato Test Uditivo</string>
|
||||||
<string name="att_manager_is_null_try_reconnecting">ATT Manager è nullo, prova a riconnetterti.</string>
|
<string name="att_manager_is_null_try_reconnecting">ATT Manager è nullo, prova a riconnetterti.</string>
|
||||||
<string name="permissions_required">Sono richieste le seguenti autorizzazioni per utilizzare l'app. Si prega di concederle per continuare.</string>
|
<string name="permissions_required">Sono richieste le seguenti autorizzazioni per utilizzare l'app. Si prega di concederle per continuare.</string>
|
||||||
<string name="shake_your_head_or_nod">Scuoti la testa o annuisci!</string>
|
<string name="shake_your_head_or_nod">Scuoti la testa o annuisci!</string>
|
||||||
<string name="root_access_required">Accesso Root Richiesto</string>
|
<string name="root_access_required">Accesso Root Richiesto</string>
|
||||||
<string name="this_app_needs_root_access_to_hook_onto_the_bluetooth_library">Questa app ha bisogno dell'accesso root per agganciarsi alla libreria Bluetooth</string>
|
<string name="this_app_needs_root_access_to_hook_onto_the_bluetooth_library">Questa app ha bisogno dell'accesso root per agganciarsi alla libreria Bluetooth</string>
|
||||||
<string name="root_access_denied">L'accesso root è stato negato. Si prega di concedere i permessi di root.</string>
|
<string name="root_access_denied">L'accesso root è stato negato. Si prega di concedere i permessi di root.</string>
|
||||||
<string name="troubleshooting_steps">Passaggi per la Risoluzione dei Problemi</string>
|
<string name="troubleshooting_steps">Passaggi per la Risoluzione dei Problemi</string>
|
||||||
<string name="hearing_test_value_instruction">Si prega di inserire i valori di perdita in dbHL</string>
|
<string name="hearing_test_value_instruction">Si prega di inserire i valori di perdita in dbHL</string>
|
||||||
<string name="about">Informazioni</string>
|
<string name="about">Informazioni</string>
|
||||||
<string name="model_name">Nome Modello</string>
|
<string name="model_name">Nome Modello</string>
|
||||||
<string name="model_number">Numero Modello</string>
|
<string name="model_number">Numero Modello</string>
|
||||||
<string name="serial_number">Numero di Serie</string>
|
<string name="serial_number">Numero di Serie</string>
|
||||||
<string name="version">Versione</string>
|
<string name="version">Versione</string>
|
||||||
<string name="hearing_health">Salute Uditiva</string>
|
<string name="hearing_health">Salute Uditiva</string>
|
||||||
<string name="hearing_protection">Protezione dell'Udito</string>
|
<string name="hearing_protection">Protezione dell'Udito</string>
|
||||||
<string name="workspace_use">Uso in Ambienti di Lavoro</string>
|
<string name="workspace_use">Uso in Ambienti di Lavoro</string>
|
||||||
<string name="ppe">Protezione EN 352</string>
|
<string name="ppe">Protezione EN 352</string>
|
||||||
<string name="workspace_use_description">La protezione EN 352 limita il livello massimo dei media a 82 dBA e soddisfa i requisiti applicabili dello standard EN 352 per la protezione individuale dell'udito.</string>
|
<string name="workspace_use_description">La protezione EN 352 limita il livello massimo dei media a 82 dBA e soddisfa i requisiti applicabili dello standard EN 352 per la protezione individuale dell'udito.</string>
|
||||||
<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="listening_mode_off_description">Disattiva la gestione del rumore</string>
|
||||||
<string name="never_show_again">Non mostrare più</string>
|
<string name="listening_mode_transparency_description">Lascia entrare i suoni esterni</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="listening_mode_adaptive_description">Regola dinamicamente il rumore esterno</string>
|
||||||
<string name="support_librepods">Supporta LibrePods</string>
|
<string name="listening_mode_noise_cancellation_description">Blocca i suoni esterni</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_adaptive_description">Regola dinamicamente il rumore esterno</string>
|
|
||||||
<string name="listening_mode_noise_cancellation_description">Blocca i suoni esterni</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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,12 +207,8 @@
|
|||||||
<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>
|
||||||
<string name="listening_mode_noise_cancellation_description">阻隔外部声音</string>
|
<string name="listening_mode_noise_cancellation_description">阻隔外部声音</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -208,12 +208,8 @@
|
|||||||
<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>
|
||||||
<string name="listening_mode_noise_cancellation_description">阻隔外部聲音</string>
|
<string name="listening_mode_noise_cancellation_description">阻隔外部聲音</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -22,4 +22,17 @@ kotlin.code.style=official
|
|||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
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