mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-07-28 08:19:48 +02:00
android: something
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -27,10 +26,10 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.kavishdevar.librepods"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
minSdk = 36
|
minSdk = 33
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 21
|
versionCode = 27
|
||||||
versionName = "0.2.0-beta.1"
|
versionName = "0.2.0"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -45,14 +44,19 @@ android {
|
|||||||
arguments += "-DCMAKE_BUILD_TYPE=Release"
|
arguments += "-DCMAKE_BUILD_TYPE=Release"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildConfigField("Boolean", "PLAY_BUILD", "false")
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
buildConfigField("Boolean", "PLAY_BUILD", "false")
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
create("playRelease") {
|
create("playRelease") {
|
||||||
initWith(getByName("release"))
|
initWith(getByName("release"))
|
||||||
versionNameSuffix = "-play"
|
buildConfigField("Boolean", "PLAY_BUILD", "true")
|
||||||
|
}
|
||||||
|
create("playDebug") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
buildConfigField("Boolean", "PLAY_BUILD", "true")
|
buildConfigField("Boolean", "PLAY_BUILD", "true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,11 +64,6 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
@@ -105,7 +104,7 @@ android {
|
|||||||
arguments += "-DIS_XPOSED=ON"
|
arguments += "-DIS_XPOSED=ON"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applicationIdSuffix = ".xposed"
|
versionNameSuffix = "-xposed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,9 +133,10 @@ dependencies {
|
|||||||
implementation(libs.aboutlibraries)
|
implementation(libs.aboutlibraries)
|
||||||
implementation(libs.aboutlibraries.compose.m3)
|
implementation(libs.aboutlibraries.compose.m3)
|
||||||
implementation(libs.backdrop)
|
implementation(libs.backdrop)
|
||||||
implementation(libs.hilt)
|
// implementation(libs.hilt)
|
||||||
// implementation(libs.hilt.compiler)
|
// implementation(libs.hilt.compiler)
|
||||||
add("xposedCompileOnly", files("libs/libxposed-api-100.aar"))
|
add("xposedCompileOnly", libs.libxposed.api)
|
||||||
|
add("xposedImplementation", libs.libxposed.service)
|
||||||
add("playReleaseImplementation", libs.billing)
|
add("playReleaseImplementation", libs.billing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".LibrePodsApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
android:description="@string/app_description"
|
android:description="@string/app_description"
|
||||||
tools:ignore="UnusedAttribute" >
|
tools:ignore="UnusedAttribute" >
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widgets.NoiseControlWidget"
|
android:name=".presentation.widgets.NoiseControlWidget"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widgets.BatteryWidget"
|
android:name=".presentation.widgets.BatteryWidget"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package me.kavishdevar.librepods
|
|||||||
|
|
||||||
// import me.kavishdevar.librepods.screens.Onboarding
|
// import me.kavishdevar.librepods.screens.Onboarding
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
|
//import dagger.hilt.android.AndroidEntryPoint
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
@@ -51,7 +52,9 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
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.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -61,6 +64,7 @@ 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.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -77,6 +81,8 @@ 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
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
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
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -88,6 +94,8 @@ 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.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -111,40 +119,44 @@ 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.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.billing.BillingManager
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
|
||||||
import me.kavishdevar.librepods.data.ControlCommandRepository
|
import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||||
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
|
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
||||||
import me.kavishdevar.librepods.screens.AdaptiveStrengthScreen
|
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||||
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.AppSettingsScreen
|
import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
|
||||||
import me.kavishdevar.librepods.screens.CameraControlScreen
|
import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.DebugScreen
|
import me.kavishdevar.librepods.presentation.screens.AppSettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
import me.kavishdevar.librepods.presentation.screens.CameraControlScreen
|
||||||
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
|
import me.kavishdevar.librepods.presentation.screens.DebugScreen
|
||||||
import me.kavishdevar.librepods.screens.HearingAidScreen
|
import me.kavishdevar.librepods.presentation.screens.HeadTrackingScreen
|
||||||
import me.kavishdevar.librepods.screens.HearingProtectionScreen
|
import me.kavishdevar.librepods.presentation.screens.HearingAidAdjustmentsScreen
|
||||||
import me.kavishdevar.librepods.screens.LongPress
|
import me.kavishdevar.librepods.presentation.screens.HearingAidScreen
|
||||||
import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen
|
import me.kavishdevar.librepods.presentation.screens.HearingProtectionScreen
|
||||||
import me.kavishdevar.librepods.screens.RenameScreen
|
import me.kavishdevar.librepods.presentation.screens.LongPress
|
||||||
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
|
import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen
|
||||||
import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
|
import me.kavishdevar.librepods.presentation.screens.PurchaseScreen
|
||||||
import me.kavishdevar.librepods.screens.VersionScreen
|
import me.kavishdevar.librepods.presentation.screens.RenameScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.TransparencySettingsScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.UpdateHearingTestScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.screens.VersionScreen
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
|
||||||
import me.kavishdevar.librepods.utils.isSupported
|
import me.kavishdevar.librepods.utils.isSupported
|
||||||
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
|
//@AndroidEntryPoint
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -161,7 +173,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
LibrePodsTheme {
|
_root_ide_package_.me.kavishdevar.librepods.presentation.theme.LibrePodsTheme {
|
||||||
Main()
|
Main()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,28 +215,153 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag")
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main() {
|
fun Main() {
|
||||||
if (!isSupported()) {
|
val context = LocalContext.current
|
||||||
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
if (!isSupported(sharedPreferences)) {
|
||||||
|
val showDialog = remember { mutableStateOf(false) }
|
||||||
|
val blockTouches = remember { mutableStateOf(false) }
|
||||||
|
val tapCount = remember { mutableIntStateOf(0) }
|
||||||
|
val lastTapTime = remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
|
val hazeState = rememberHazeState()
|
||||||
|
|
||||||
|
LaunchedEffect(blockTouches) {
|
||||||
|
if (blockTouches.value) {
|
||||||
|
delay(500)
|
||||||
|
blockTouches.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.hazeSource(hazeState)
|
||||||
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)),
|
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(
|
||||||
|
if (blockTouches.value)
|
||||||
|
{
|
||||||
|
Modifier.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||||
|
event.changes.forEach { it.consume() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column (
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
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.",
|
text = "Not supported",
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
fontSize = 20.sp
|
||||||
|
),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Row (
|
||||||
|
modifier = Modifier.fillMaxWidth().pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (now - lastTapTime.longValue > 400) {
|
||||||
|
tapCount.intValue = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCount.intValue++
|
||||||
|
lastTapTime.longValue = now
|
||||||
|
|
||||||
|
if (tapCount.intValue >= 7) {
|
||||||
|
showDialog.value = true
|
||||||
|
blockTouches.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Device Info:",
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
"MANUFACTURER=${Build.MANUFACTURER}\n" +
|
||||||
|
"MODEL=${Build.MODEL}\n" +
|
||||||
|
"BUILD_ID=${Build.ID}\n" +
|
||||||
|
"SDK_INT_FULL= ${Build.VERSION.SDK_INT_FULL}\n",
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily(Font(R.font.hack)),
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
text = "Check the repository for more info.",
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
fontSize = 18.sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmationDialog(
|
||||||
|
showDialog = showDialog,
|
||||||
|
title = "Confirm device check bypass?",
|
||||||
|
message = "Are you sure your device is supported with LibrePods?",
|
||||||
|
confirmText = "Yes",
|
||||||
|
dismissText = "No",
|
||||||
|
onConfirm = {
|
||||||
|
showDialog.value = false
|
||||||
|
sharedPreferences.edit {
|
||||||
|
tapCount.intValue = 0
|
||||||
|
putBoolean("bypass_device_check", true)
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showDialog.value = false
|
||||||
|
},
|
||||||
|
hazeState = hazeState
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val isConnected = remember { mutableStateOf(false) }
|
val isConnected = remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
|
||||||
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
||||||
val overlaySkipped = remember {
|
val overlaySkipped = remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
@@ -263,7 +400,7 @@ fun Main() {
|
|||||||
|
|
||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
|
|
||||||
val viewModel = remember(airPodsService.value) {
|
val airPodsViewModel = remember(airPodsService.value) {
|
||||||
airPodsService.value?.let { service ->
|
airPodsService.value?.let { service ->
|
||||||
AirPodsViewModel(
|
AirPodsViewModel(
|
||||||
service = service,
|
service = service,
|
||||||
@@ -317,19 +454,20 @@ fun Main() {
|
|||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
if (viewModel != null) AirPodsSettingsScreen(viewModel, navController)
|
if (airPodsViewModel != null) AirPodsSettingsScreen(airPodsViewModel, navController)
|
||||||
}
|
}
|
||||||
composable("debug") {
|
composable("debug") {
|
||||||
DebugScreen(navController = navController)
|
DebugScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable("long_press/{bud}") { navBackStackEntry ->
|
composable("long_press/{bud}") { navBackStackEntry ->
|
||||||
if (viewModel != null) LongPress(
|
if (airPodsViewModel != null) LongPress(
|
||||||
viewModel = viewModel,
|
viewModel = airPodsViewModel,
|
||||||
name = navBackStackEntry.arguments?.getString("bud")!!
|
name = navBackStackEntry.arguments?.getString("bud")!!,
|
||||||
|
navController = navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("rename") {
|
composable("rename") {
|
||||||
if (viewModel != null) RenameScreen(viewModel)
|
if (airPodsViewModel != null) RenameScreen(airPodsViewModel)
|
||||||
}
|
}
|
||||||
composable("app_settings") {
|
composable("app_settings") {
|
||||||
val appSettingsViewModel: AppSettingsViewModel = viewModel()
|
val appSettingsViewModel: AppSettingsViewModel = viewModel()
|
||||||
@@ -339,37 +477,41 @@ fun Main() {
|
|||||||
// TroubleshootingScreen(navController)
|
// TroubleshootingScreen(navController)
|
||||||
// }
|
// }
|
||||||
composable("head_tracking") {
|
composable("head_tracking") {
|
||||||
if (viewModel != null) HeadTrackingScreen(viewModel)
|
if (airPodsViewModel != null) HeadTrackingScreen(airPodsViewModel, navController)
|
||||||
}
|
}
|
||||||
composable("accessibility") {
|
composable("accessibility") {
|
||||||
if (viewModel != null) AccessibilitySettingsScreen(viewModel, navController)
|
if (airPodsViewModel != null) AccessibilitySettingsScreen(airPodsViewModel, navController)
|
||||||
}
|
}
|
||||||
composable("transparency_customization") {
|
composable("transparency_customization") {
|
||||||
if (viewModel != null) TransparencySettingsScreen(viewModel)
|
if (airPodsViewModel != null) TransparencySettingsScreen(airPodsViewModel)
|
||||||
}
|
}
|
||||||
composable("hearing_aid") {
|
composable("hearing_aid") {
|
||||||
if (viewModel != null) HearingAidScreen(viewModel, navController)
|
if (airPodsViewModel != null) HearingAidScreen(airPodsViewModel, navController)
|
||||||
}
|
}
|
||||||
composable("hearing_aid_adjustments") {
|
composable("hearing_aid_adjustments") {
|
||||||
if (viewModel != null) HearingAidAdjustmentsScreen(viewModel)
|
if (airPodsViewModel != null) HearingAidAdjustmentsScreen(airPodsViewModel)
|
||||||
}
|
}
|
||||||
composable("adaptive_strength") {
|
composable("adaptive_strength") {
|
||||||
if (viewModel != null) AdaptiveStrengthScreen(viewModel)
|
if (airPodsViewModel != null) AdaptiveStrengthScreen(airPodsViewModel, navController)
|
||||||
}
|
}
|
||||||
composable("camera_control") {
|
composable("camera_control") {
|
||||||
if (viewModel != null) CameraControlScreen(viewModel)
|
if (airPodsViewModel != null) CameraControlScreen(airPodsViewModel)
|
||||||
}
|
}
|
||||||
composable("open_source_licenses") {
|
composable("open_source_licenses") {
|
||||||
OpenSourceLicensesScreen(navController)
|
OpenSourceLicensesScreen(navController)
|
||||||
}
|
}
|
||||||
composable("update_hearing_test") {
|
composable("update_hearing_test") {
|
||||||
if (viewModel != null) UpdateHearingTestScreen()
|
if (airPodsViewModel != null) UpdateHearingTestScreen()
|
||||||
}
|
}
|
||||||
composable("version_info") {
|
composable("version_info") {
|
||||||
if (viewModel != null) VersionScreen(viewModel)
|
if (airPodsViewModel != null) VersionScreen(airPodsViewModel)
|
||||||
}
|
}
|
||||||
composable("hearing_protection") {
|
composable("hearing_protection") {
|
||||||
if (viewModel != null) HearingProtectionScreen(viewModel)
|
if (airPodsViewModel != null) HearingProtectionScreen(airPodsViewModel)
|
||||||
|
}
|
||||||
|
composable("purchase_screen") {
|
||||||
|
val purchaseViewModel: PurchaseViewModel = viewModel()
|
||||||
|
PurchaseScreen(purchaseViewModel, navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,15 +85,15 @@ 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 kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.composables.AdaptiveRainbowBrush
|
import me.kavishdevar.librepods.presentation.components.AdaptiveRainbowBrush
|
||||||
import me.kavishdevar.librepods.composables.ControlCenterNoiseControlSegmentedButton
|
import me.kavishdevar.librepods.presentation.components.ControlCenterNoiseControlSegmentedButton
|
||||||
import me.kavishdevar.librepods.composables.IconAreaSize
|
import me.kavishdevar.librepods.presentation.components.IconAreaSize
|
||||||
import me.kavishdevar.librepods.composables.VerticalVolumeSlider
|
import me.kavishdevar.librepods.presentation.components.VerticalVolumeSlider
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
import me.kavishdevar.librepods.data.NoiseControlMode
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
|
|
||||||
interface BillingProvider {
|
interface BillingProvider {
|
||||||
val isPremium: StateFlow<Boolean>
|
val isPremium: StateFlow<Boolean>
|
||||||
|
val price: StateFlow<String>
|
||||||
fun purchase(activity: Activity)
|
fun purchase(activity: Activity)
|
||||||
|
fun queryPurchases()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ object BillingProviderFactory {
|
|||||||
return if (BuildConfig.PLAY_BUILD) {
|
return if (BuildConfig.PLAY_BUILD) {
|
||||||
PlayBillingProvider(context)
|
PlayBillingProvider(context)
|
||||||
} else {
|
} else {
|
||||||
FOSSBillingProvider()
|
FOSSBillingProvider(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,56 @@
|
|||||||
package me.kavishdevar.librepods.billing
|
package me.kavishdevar.librepods.billing
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class FOSSBillingProvider : BillingProvider {
|
class FOSSBillingProvider(context: Context): BillingProvider {
|
||||||
private val _isPremium = MutableStateFlow(true)
|
private val _isPremium = MutableStateFlow(false)
|
||||||
override val isPremium: StateFlow<Boolean> = _isPremium
|
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||||
|
|
||||||
override fun purchase(activity: Activity) { }
|
private val _price = MutableStateFlow("Any")
|
||||||
|
override val price: StateFlow<String> = _price
|
||||||
|
|
||||||
|
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private var purchaseJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
queryPurchases()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun purchase(activity: Activity) {
|
||||||
|
activity.startActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW, "https://github.com/sponsors/kavishdevar".toUri())
|
||||||
|
)
|
||||||
|
|
||||||
|
purchaseJob?.cancel()
|
||||||
|
|
||||||
|
purchaseJob = scope.launch {
|
||||||
|
delay(2_000)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_isPremium.value = true
|
||||||
|
sharedPreferences.edit { putBoolean("foss_upgraded", true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryPurchases() {
|
||||||
|
val stored = sharedPreferences.getBoolean("foss_upgraded", false)
|
||||||
|
if (stored != _isPremium.value) {
|
||||||
|
_isPremium.value = stored
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ class PlayBillingProvider(
|
|||||||
private val _isPremium = MutableStateFlow(false)
|
private val _isPremium = MutableStateFlow(false)
|
||||||
override val isPremium: StateFlow<Boolean> = _isPremium
|
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||||
|
|
||||||
|
private val _price = MutableStateFlow("unknown")
|
||||||
|
override val price: StateFlow<String> = _price
|
||||||
|
|
||||||
|
|
||||||
private var productDetails: ProductDetails? = null
|
private var productDetails: ProductDetails? = null
|
||||||
|
|
||||||
private val billingClient = BillingClient.newBuilder(context)
|
private val billingClient = BillingClient.newBuilder(context)
|
||||||
@@ -102,6 +106,13 @@ class PlayBillingProvider(
|
|||||||
if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
productDetails = result.productDetailsList?.firstOrNull()
|
productDetails = result.productDetailsList?.firstOrNull()
|
||||||
Log.d(TAG, "Product loaded: ${productDetails?.name}")
|
Log.d(TAG, "Product loaded: ${productDetails?.name}")
|
||||||
|
val priceString = productDetails
|
||||||
|
?.oneTimePurchaseOfferDetails
|
||||||
|
?.formattedPrice
|
||||||
|
|
||||||
|
if (priceString != null) {
|
||||||
|
_price.value = priceString
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "queryProductDetails failed: ${result.billingResult.debugMessage}")
|
Log.w(TAG, "queryProductDetails failed: ${result.billingResult.debugMessage}")
|
||||||
}
|
}
|
||||||
@@ -152,13 +163,13 @@ class PlayBillingProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// val purchase = purchases.find {
|
// val navigateToPurchase = purchases.find {
|
||||||
// it.products.contains(PREMIUM_PRODUCT_ID) && it.purchaseState == Purchase.PurchaseState.PURCHASED
|
// it.products.contains(PREMIUM_PRODUCT_ID) && it.purchaseState == Purchase.PurchaseState.PURCHASED
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// if (purchase != null) {
|
// if (navigateToPurchase != null) {
|
||||||
// val consumeParams = ConsumeParams.newBuilder()
|
// val consumeParams = ConsumeParams.newBuilder()
|
||||||
// .setPurchaseToken(purchase.purchaseToken)
|
// .setPurchaseToken(navigateToPurchase.purchaseToken)
|
||||||
// .build()
|
// .build()
|
||||||
// scope.launch {
|
// scope.launch {
|
||||||
// billingClient.consumeAsync(consumeParams) { _, _ ->}
|
// billingClient.consumeAsync(consumeParams) { _, _ ->}
|
||||||
@@ -184,4 +195,10 @@ class PlayBillingProvider(
|
|||||||
Log.e(TAG, "Acknowledgement failed: ${result.debugMessage}")
|
Log.e(TAG, "Acknowledgement failed: ${result.debugMessage}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun queryPurchases() {
|
||||||
|
scope.launch {
|
||||||
|
queryExistingPurchases()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-138
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.bluetooth
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
@@ -61,8 +61,7 @@ class AACPManager {
|
|||||||
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
|
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
|
||||||
|
|
||||||
data class ControlCommandStatus(
|
data class ControlCommandStatus(
|
||||||
val identifier: ControlCommandIdentifiers,
|
val identifier: ControlCommandIdentifiers, val value: ByteArray
|
||||||
val value: ByteArray
|
|
||||||
) {
|
) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -85,42 +84,31 @@ class AACPManager {
|
|||||||
|
|
||||||
// @Suppress("unused")
|
// @Suppress("unused")
|
||||||
enum class ControlCommandIdentifiers(val value: Byte) {
|
enum class ControlCommandIdentifiers(val value: Byte) {
|
||||||
MIC_MODE(0x01),
|
MIC_MODE(0x01), BUTTON_SEND_MODE(0x05), VOICE_TRIGGER(0x12), SINGLE_CLICK_MODE(0x14), DOUBLE_CLICK_MODE(
|
||||||
BUTTON_SEND_MODE(0x05),
|
0x15
|
||||||
VOICE_TRIGGER(0x12),
|
),
|
||||||
SINGLE_CLICK_MODE(0x14),
|
CLICK_HOLD_MODE(0x16), DOUBLE_CLICK_INTERVAL(0x17), CLICK_HOLD_INTERVAL(0x18), LISTENING_MODE_CONFIGS(
|
||||||
DOUBLE_CLICK_MODE(0x15),
|
0x1A
|
||||||
CLICK_HOLD_MODE(0x16),
|
),
|
||||||
DOUBLE_CLICK_INTERVAL(0x17),
|
ONE_BUD_ANC_MODE(0x1B), CROWN_ROTATION_DIRECTION(0x1C), LISTENING_MODE(0x0D), AUTO_ANSWER_MODE(
|
||||||
CLICK_HOLD_INTERVAL(0x18),
|
0x1E
|
||||||
LISTENING_MODE_CONFIGS(0x1A),
|
),
|
||||||
ONE_BUD_ANC_MODE(0x1B),
|
CHIME_VOLUME(0x1F), VOLUME_SWIPE_INTERVAL(0x23), CALL_MANAGEMENT_CONFIG(0x24), VOLUME_SWIPE_MODE(
|
||||||
CROWN_ROTATION_DIRECTION(0x1C),
|
0x25
|
||||||
LISTENING_MODE(0x0D),
|
),
|
||||||
AUTO_ANSWER_MODE(0x1E),
|
ADAPTIVE_VOLUME_CONFIG(0x26), SOFTWARE_MUTE_CONFIG(0x27), CONVERSATION_DETECT_CONFIG(
|
||||||
CHIME_VOLUME(0x1F),
|
0x28
|
||||||
VOLUME_SWIPE_INTERVAL(0x23),
|
),
|
||||||
CALL_MANAGEMENT_CONFIG(0x24),
|
SSL(0x29), HEARING_AID(0x2C), AUTO_ANC_STRENGTH(0x2E), HPS_GAIN_SWIPE(0x2F), HRM_STATE(
|
||||||
VOLUME_SWIPE_MODE(0x25),
|
0x30
|
||||||
ADAPTIVE_VOLUME_CONFIG(0x26),
|
),
|
||||||
SOFTWARE_MUTE_CONFIG(0x27),
|
IN_CASE_TONE_CONFIG(0x31), SIRI_MULTITONE_CONFIG(0x32), HEARING_ASSIST_CONFIG(0x33), ALLOW_OFF_OPTION(
|
||||||
CONVERSATION_DETECT_CONFIG(0x28),
|
0x34
|
||||||
SSL(0x29),
|
),
|
||||||
HEARING_AID(0x2C),
|
STEM_CONFIG(0x39), SLEEP_DETECTION_CONFIG(0x35), ALLOW_AUTO_CONNECT(0x36), // not sure what this does, AUTOMATIC_CONNECTION is the only one used, but this is newer... so ¯\_(ツ)_/¯
|
||||||
AUTO_ANC_STRENGTH(0x2E),
|
EAR_DETECTION_CONFIG(0x0A), AUTOMATIC_CONNECTION_CONFIG(0x20), OWNS_CONNECTION(0x06), PPE_TOGGLE_CONFIG(
|
||||||
HPS_GAIN_SWIPE(0x2F),
|
0x37
|
||||||
HRM_STATE(0x30),
|
),
|
||||||
IN_CASE_TONE_CONFIG(0x31),
|
|
||||||
SIRI_MULTITONE_CONFIG(0x32),
|
|
||||||
HEARING_ASSIST_CONFIG(0x33),
|
|
||||||
ALLOW_OFF_OPTION(0x34),
|
|
||||||
STEM_CONFIG(0x39),
|
|
||||||
SLEEP_DETECTION_CONFIG(0x35),
|
|
||||||
ALLOW_AUTO_CONNECT(0x36), // not sure what this does, AUTOMATIC_CONNECTION is the only one used, but this is newer... so ¯\_(ツ)_/¯
|
|
||||||
EAR_DETECTION_CONFIG(0x0A),
|
|
||||||
AUTOMATIC_CONNECTION_CONFIG(0x20),
|
|
||||||
OWNS_CONNECTION(0x06),
|
|
||||||
PPE_TOGGLE_CONFIG(0x37),
|
|
||||||
PPE_CAP_LEVEL_CONFIG(0x38);
|
PPE_CAP_LEVEL_CONFIG(0x38);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -130,59 +118,44 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class ProximityKeyType(val value: Byte) {
|
enum class ProximityKeyType(val value: Byte) {
|
||||||
IRK(0x01),
|
IRK(0x01), ENC_KEY(0x04);
|
||||||
ENC_KEY(0x04);
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromByte(byte: Byte): ProximityKeyType =
|
fun fromByte(byte: Byte): ProximityKeyType = entries.find { it.value == byte }
|
||||||
ProximityKeyType.entries.find { it.value == byte }
|
|
||||||
?: throw IllegalArgumentException("Unknown ProximityKeyType: $byte")
|
?: throw IllegalArgumentException("Unknown ProximityKeyType: $byte")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class StemPressType(val value: Byte) {
|
enum class StemPressType(val value: Byte) {
|
||||||
SINGLE_PRESS(0x05),
|
SINGLE_PRESS(0x05), DOUBLE_PRESS(0x06), TRIPLE_PRESS(0x07), LONG_PRESS(0x08);
|
||||||
DOUBLE_PRESS(0x06),
|
|
||||||
TRIPLE_PRESS(0x07),
|
|
||||||
LONG_PRESS(0x08);
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromByte(byte: Byte): StemPressType? =
|
fun fromByte(byte: Byte): StemPressType? = entries.find { it.value == byte }
|
||||||
entries.find { it.value == byte }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class StemPressBudType(val value: Byte) {
|
enum class StemPressBudType(val value: Byte) {
|
||||||
LEFT(0x01),
|
LEFT(0x01), RIGHT(0x02);
|
||||||
RIGHT(0x02);
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromByte(byte: Byte): StemPressBudType? =
|
fun fromByte(byte: Byte): StemPressBudType? = entries.find { it.value == byte }
|
||||||
entries.find { it.value == byte }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class AudioSourceType(val value: Byte) {
|
enum class AudioSourceType(val value: Byte) {
|
||||||
NONE(0x00),
|
NONE(0x00), CALL(0x01), MEDIA(0x02);
|
||||||
CALL(0x01),
|
|
||||||
MEDIA(0x02);
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromByte(byte: Byte): AudioSourceType? =
|
fun fromByte(byte: Byte): AudioSourceType? = entries.find { it.value == byte }
|
||||||
entries.find { it.value == byte }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AudioSource(
|
data class AudioSource(
|
||||||
val mac: String,
|
val mac: String, val type: AudioSourceType
|
||||||
val type: AudioSourceType
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ConnectedDevice(
|
data class ConnectedDevice(
|
||||||
val mac: String,
|
val mac: String, val info1: Byte, val info2: Byte, var type: String?
|
||||||
val info1: Byte,
|
|
||||||
val info2: Byte,
|
|
||||||
var type: String?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AirPodsInformation(
|
data class AirPodsInformation(
|
||||||
@@ -231,8 +204,7 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setControlCommandStatusValue(
|
private fun setControlCommandStatusValue(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, value: ByteArray
|
||||||
value: ByteArray
|
|
||||||
) {
|
) {
|
||||||
val existingStatus = getControlCommandStatus(identifier)
|
val existingStatus = getControlCommandStatus(identifier)
|
||||||
if (existingStatus == value) {
|
if (existingStatus == value) {
|
||||||
@@ -289,15 +261,13 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun registerControlCommandListener(
|
fun registerControlCommandListener(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, callback: ControlCommandListener
|
||||||
callback: ControlCommandListener
|
|
||||||
) {
|
) {
|
||||||
controlCommandListeners.getOrPut(identifier) { mutableListOf() }.add(callback)
|
controlCommandListeners.getOrPut(identifier) { mutableListOf() }.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterControlCommandListener(
|
fun unregisterControlCommandListener(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, callback: ControlCommandListener
|
||||||
callback: ControlCommandListener
|
|
||||||
) {
|
) {
|
||||||
controlCommandListeners[identifier]?.remove(callback)
|
controlCommandListeners[identifier]?.remove(callback)
|
||||||
}
|
}
|
||||||
@@ -332,8 +302,7 @@ class AACPManager {
|
|||||||
fun sendControlCommand(identifier: Byte, value: ByteArray): Boolean {
|
fun sendControlCommand(identifier: Byte, value: ByteArray): Boolean {
|
||||||
val controlPacket = createControlCommandPacket(identifier, value)
|
val controlPacket = createControlCommandPacket(identifier, value)
|
||||||
setControlCommandStatusValue(
|
setControlCommandStatusValue(
|
||||||
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
|
ControlCommandIdentifiers.fromByte(identifier) ?: return false, value
|
||||||
value
|
|
||||||
)
|
)
|
||||||
return sendDataPacket(controlPacket)
|
return sendDataPacket(controlPacket)
|
||||||
}
|
}
|
||||||
@@ -342,16 +311,14 @@ class AACPManager {
|
|||||||
fun sendControlCommand(identifier: Byte, value: Byte): Boolean {
|
fun sendControlCommand(identifier: Byte, value: Byte): Boolean {
|
||||||
val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value))
|
val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value))
|
||||||
setControlCommandStatusValue(
|
setControlCommandStatusValue(
|
||||||
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
|
ControlCommandIdentifiers.fromByte(identifier) ?: return false, byteArrayOf(value)
|
||||||
byteArrayOf(value)
|
|
||||||
)
|
)
|
||||||
return sendDataPacket(controlPacket)
|
return sendDataPacket(controlPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendControlCommand(identifier: Byte, value: Boolean): Boolean {
|
fun sendControlCommand(identifier: Byte, value: Boolean): Boolean {
|
||||||
val controlPacket = createControlCommandPacket(
|
val controlPacket = createControlCommandPacket(
|
||||||
identifier,
|
identifier, if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
|
||||||
if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
|
|
||||||
)
|
)
|
||||||
setControlCommandStatusValue(
|
setControlCommandStatusValue(
|
||||||
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
|
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
|
||||||
@@ -371,8 +338,7 @@ class AACPManager {
|
|||||||
|
|
||||||
fun parseProximityKeysResponse(data: ByteArray): Map<ProximityKeyType, ByteArray> {
|
fun parseProximityKeysResponse(data: ByteArray): Map<ProximityKeyType, ByteArray> {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG, "Parsing Proximity Keys Response: ${data.joinToString(" ") { "%02X".format(it) }}"
|
||||||
"Parsing Proximity Keys Response: ${data.joinToString(" ") { "%02X".format(it) }}"
|
|
||||||
)
|
)
|
||||||
if (data.size < 4) {
|
if (data.size < 4) {
|
||||||
throw IllegalArgumentException("Data array too short to parse Proximity Keys Response")
|
throw IllegalArgumentException("Data array too short to parse Proximity Keys Response")
|
||||||
@@ -400,11 +366,9 @@ class AACPManager {
|
|||||||
keys[ProximityKeyType.fromByte(keyType)] = key
|
keys[ProximityKeyType.fromByte(keyType)] = key
|
||||||
offset += keyLength
|
offset += keyLength
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG, "Parsed Proximity Key: Type: ${keyType}, Length: $keyLength, Key: ${
|
||||||
"Parsed Proximity Key: Type: ${keyType}, Length: $keyLength, Key: ${
|
|
||||||
key.joinToString(" ") { "%02X".format(it) }
|
key.joinToString(" ") { "%02X".format(it) }
|
||||||
}"
|
}")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
@@ -424,26 +388,21 @@ class AACPManager {
|
|||||||
fun receivePacket(packet: ByteArray) {
|
fun receivePacket(packet: ByteArray) {
|
||||||
if (!packet.toHexString().startsWith("04000400")) {
|
if (!packet.toHexString().startsWith("04000400")) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG, "Received packet does not start with expected header: ${
|
||||||
"Received packet does not start with expected header: ${
|
|
||||||
packet.joinToString(" ") {
|
packet.joinToString(" ") {
|
||||||
"%02X".format(it)
|
"%02X".format(it)
|
||||||
}
|
}
|
||||||
}"
|
}")
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (packet.size < 6) {
|
if (packet.size < 6) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG, "Received packet too short: ${packet.joinToString(" ") { "%02X".format(it) }}"
|
||||||
"Received packet too short: ${packet.joinToString(" ") { "%02X".format(it) }}"
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val opcode = packet[4]
|
when (val opcode = packet[4]) {
|
||||||
|
|
||||||
when (opcode) {
|
|
||||||
Opcodes.BATTERY_INFO -> {
|
Opcodes.BATTERY_INFO -> {
|
||||||
callback?.onBatteryInfoReceived(packet)
|
callback?.onBatteryInfoReceived(packet)
|
||||||
}
|
}
|
||||||
@@ -458,8 +417,7 @@ class AACPManager {
|
|||||||
TAG,
|
TAG,
|
||||||
"Control command received: ${controlCommand.identifier.toHexString()} - ${
|
"Control command received: ${controlCommand.identifier.toHexString()} - ${
|
||||||
controlCommand.value.joinToString(" ") { "%02X".format(it) }
|
controlCommand.value.joinToString(" ") { "%02X".format(it) }
|
||||||
}"
|
}")
|
||||||
)
|
|
||||||
|
|
||||||
val controlCommandListText = try {
|
val controlCommandListText = try {
|
||||||
controlCommandStatusList.joinToString(", ") { it ->
|
controlCommandStatusList.joinToString(", ") { it ->
|
||||||
@@ -474,7 +432,8 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG, "Control command list is now: $controlCommandListText")
|
TAG, "Control command list is now: $controlCommandListText"
|
||||||
|
)
|
||||||
|
|
||||||
val controlCommandIdentifier =
|
val controlCommandIdentifier =
|
||||||
ControlCommandIdentifiers.fromByte(controlCommand.identifier)
|
ControlCommandIdentifiers.fromByte(controlCommand.identifier)
|
||||||
@@ -508,13 +467,11 @@ class AACPManager {
|
|||||||
Opcodes.HEADTRACKING -> {
|
Opcodes.HEADTRACKING -> {
|
||||||
if (packet.size < 70) {
|
if (packet.size < 70) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG, "Received HEADTRACKING packet too short: ${
|
||||||
"Received HEADTRACKING packet too short: ${
|
|
||||||
packet.joinToString(" ") {
|
packet.joinToString(" ") {
|
||||||
"%02X".format(it)
|
"%02X".format(it)
|
||||||
}
|
}
|
||||||
}"
|
}")
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
callback?.onHeadTrackingReceived(packet)
|
callback?.onHeadTrackingReceived(packet)
|
||||||
@@ -546,7 +503,8 @@ class AACPManager {
|
|||||||
|
|
||||||
Opcodes.SMART_ROUTING_RESP -> {
|
Opcodes.SMART_ROUTING_RESP -> {
|
||||||
val packetString = packet.decodeToString()
|
val packetString = packet.decodeToString()
|
||||||
val sender = packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
|
val sender =
|
||||||
|
packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
|
||||||
|
|
||||||
// if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
|
// if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
|
||||||
// val nameStartIndex = packetString.indexOf("btName") + 8
|
// val nameStartIndex = packetString.indexOf("btName") + 8
|
||||||
@@ -566,9 +524,15 @@ class AACPManager {
|
|||||||
} else if ("Android" in packetString) {
|
} else if ("Android" in packetString) {
|
||||||
connectedDevices.find { it.mac == sender }?.type = "Android"
|
connectedDevices.find { it.mac == sender }?.type = "Android"
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}")
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}"
|
||||||
|
)
|
||||||
if (packetString.contains("SetOwnershipToFalse")) {
|
if (packetString.contains("SetOwnershipToFalse")) {
|
||||||
callback?.onOwnershipToFalseRequest(sender, packetString.contains("ReverseBannerTapped"))
|
callback?.onOwnershipToFalseRequest(
|
||||||
|
sender,
|
||||||
|
packetString.contains("ReverseBannerTapped")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (packetString.contains("ShowNearbyUI")) {
|
if (packetString.contains("ShowNearbyUI")) {
|
||||||
callback?.onShowNearbyUI(sender)
|
callback?.onShowNearbyUI(sender)
|
||||||
@@ -595,15 +559,19 @@ class AACPManager {
|
|||||||
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 visible. 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 =
|
||||||
val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
ByteBuffer.wrap(packet, 12, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
|
||||||
// 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)
|
callback?.onEQPacketReceived(eqData)
|
||||||
}
|
}
|
||||||
@@ -613,8 +581,9 @@ class AACPManager {
|
|||||||
val information = parseInformationPacket(packet)
|
val information = parseInformationPacket(packet)
|
||||||
callback?.onDeviceInformationReceived(information)
|
callback?.onDeviceInformationReceived(information)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "Unknown opcode received: ${opcode.toHexString()}")
|
Log.d(TAG, "Unhandled opcode received: ${opcode.toHexString()}")
|
||||||
callback?.onUnknownPacketReceived(packet)
|
callback?.onUnknownPacketReceived(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -644,10 +613,22 @@ class AACPManager {
|
|||||||
|
|
||||||
fun createHandshakePacket(): ByteArray {
|
fun createHandshakePacket(): ByteArray {
|
||||||
return byteArrayOf(
|
return byteArrayOf(
|
||||||
0x00, 0x00, 0x04, 0x00,
|
0x00,
|
||||||
0x01, 0x00, 0x02, 0x00,
|
0x00,
|
||||||
0x00, 0x00, 0x00, 0x00,
|
0x04,
|
||||||
0x00, 0x00, 0x00, 0x00
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x02,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,17 +774,31 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendMediaInformationNewDevice(selfMacAddress: String, targetMacAddress: String): Boolean {
|
fun sendMediaInformationNewDevice(selfMacAddress: String, targetMacAddress: String): Boolean {
|
||||||
if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) {
|
if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(
|
||||||
|
Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
|
||||||
|
)
|
||||||
|
) {
|
||||||
// throw IllegalArgumentException("MAC address must be 6 bytes")
|
// throw IllegalArgumentException("MAC address must be 6 bytes")
|
||||||
Log.w(TAG, "Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress")
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress"
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: $targetMacAddress")
|
Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: $targetMacAddress")
|
||||||
Log.d(TAG, "Sending Media Information packet to $targetMacAddress")
|
Log.d(TAG, "Sending Media Information packet to $targetMacAddress")
|
||||||
return sendDataPacket(createMediaInformationNewDevicePacket(selfMacAddress, targetMacAddress))
|
return sendDataPacket(
|
||||||
|
createMediaInformationNewDevicePacket(
|
||||||
|
selfMacAddress,
|
||||||
|
targetMacAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createMediaInformationNewDevicePacket(selfMacAddress: String, targetMacAddress: String): ByteArray {
|
fun createMediaInformationNewDevicePacket(
|
||||||
|
selfMacAddress: String,
|
||||||
|
targetMacAddress: String
|
||||||
|
): ByteArray {
|
||||||
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
||||||
val buffer = ByteBuffer.allocate(116)
|
val buffer = ByteBuffer.allocate(116)
|
||||||
buffer.put(
|
buffer.put(
|
||||||
@@ -892,17 +887,13 @@ class AACPManager {
|
|||||||
Log.d(TAG, "Sending Media Information packet to $targetMac")
|
Log.d(TAG, "Sending Media Information packet to $targetMac")
|
||||||
return sendDataPacket(
|
return sendDataPacket(
|
||||||
createMediaInformationPacket(
|
createMediaInformationPacket(
|
||||||
selfMacAddress,
|
selfMacAddress, targetMac, streamingState
|
||||||
targetMac,
|
|
||||||
streamingState
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createMediaInformationPacket(
|
fun createMediaInformationPacket(
|
||||||
selfMacAddress: String,
|
selfMacAddress: String, targetMacAddress: String, streamingState: Boolean = true
|
||||||
targetMacAddress: String,
|
|
||||||
streamingState: Boolean = true
|
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
||||||
val buffer = ByteBuffer.allocate(138)
|
val buffer = ByteBuffer.allocate(138)
|
||||||
@@ -935,7 +926,7 @@ class AACPManager {
|
|||||||
buffer.put("AudioCategory".toByteArray())
|
buffer.put("AudioCategory".toByteArray())
|
||||||
buffer.put(byteArrayOf(0x31, 0x2D, 0x01))
|
buffer.put(byteArrayOf(0x31, 0x2D, 0x01))
|
||||||
|
|
||||||
return opcode+buffer.array()
|
return opcode + buffer.array()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendSmartRoutingShowUI(selfMacAddress: String): Boolean {
|
fun sendSmartRoutingShowUI(selfMacAddress: String): Boolean {
|
||||||
@@ -1017,9 +1008,15 @@ class AACPManager {
|
|||||||
|
|
||||||
|
|
||||||
fun sendAddTiPiDevice(selfMacAddress: String, targetMacAddress: String): Boolean {
|
fun sendAddTiPiDevice(selfMacAddress: String, targetMacAddress: String): Boolean {
|
||||||
if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) {
|
if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(
|
||||||
|
Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
|
||||||
|
)
|
||||||
|
) {
|
||||||
// throw IllegalArgumentException("MAC address must be 6 bytes")
|
// throw IllegalArgumentException("MAC address must be 6 bytes")
|
||||||
Log.w(TAG, "Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress")
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress"
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Sending Add TiPi Device packet to $targetMacAddress")
|
Log.d(TAG, "Sending Add TiPi Device packet to $targetMacAddress")
|
||||||
@@ -1053,8 +1050,7 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ControlCommand(
|
data class ControlCommand(
|
||||||
val identifier: Byte,
|
val identifier: Byte, val value: ByteArray
|
||||||
val value: ByteArray
|
|
||||||
) {
|
) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -1106,10 +1102,8 @@ class AACPManager {
|
|||||||
triplePressCustomized: Boolean = false,
|
triplePressCustomized: Boolean = false,
|
||||||
longPressCustomized: Boolean = false
|
longPressCustomized: Boolean = false
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val value = ((if (singlePressCustomized) 0x01 else 0) or
|
val value =
|
||||||
(if (doublePressCustomized) 0x02 else 0) or
|
((if (singlePressCustomized) 0x01 else 0) or (if (doublePressCustomized) 0x02 else 0) or (if (triplePressCustomized) 0x04 else 0) or (if (longPressCustomized) 0x08 else 0)).toByte()
|
||||||
(if (triplePressCustomized) 0x04 else 0) or
|
|
||||||
(if (longPressCustomized) 0x08 else 0)).toByte()
|
|
||||||
Log.d(TAG, "Sending Stem Config Packet with value: ${value.toHexString()}")
|
Log.d(TAG, "Sending Stem Config Packet with value: ${value.toHexString()}")
|
||||||
return sendControlCommand(
|
return sendControlCommand(
|
||||||
ControlCommandIdentifiers.STEM_CONFIG.value, value
|
ControlCommandIdentifiers.STEM_CONFIG.value, value
|
||||||
@@ -1124,19 +1118,18 @@ class AACPManager {
|
|||||||
if (packet[4] == Opcodes.CONTROL_COMMAND) {
|
if (packet[4] == Opcodes.CONTROL_COMMAND) {
|
||||||
val controlCommand = ControlCommand.fromByteArray(packet)
|
val controlCommand = ControlCommand.fromByteArray(packet)
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG, "Control command: ${controlCommand.identifier.toHexString()} - ${
|
||||||
"Control command: ${controlCommand.identifier.toHexString()} - ${
|
|
||||||
controlCommand.value.joinToString(" ") { "%02X".format(it) }
|
controlCommand.value.joinToString(" ") { "%02X".format(it) }
|
||||||
}"
|
}")
|
||||||
)
|
|
||||||
setControlCommandStatusValue(
|
setControlCommandStatusValue(
|
||||||
ControlCommandIdentifiers.fromByte(controlCommand.identifier) ?: return false,
|
ControlCommandIdentifiers.fromByte(controlCommand.identifier) ?: return false,
|
||||||
controlCommand.value
|
controlCommand.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val socket = BluetoothConnectionManager.getCurrentSocket()
|
val socket = BluetoothConnectionManager.getCurrentSocket() ?: return false
|
||||||
if (socket?.isConnected == true) {
|
|
||||||
|
if (socket.isConnected) {
|
||||||
socket.outputStream?.write(packet)
|
socket.outputStream?.write(packet)
|
||||||
socket.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
return true
|
return true
|
||||||
@@ -1213,7 +1206,10 @@ class AACPManager {
|
|||||||
var offset = 9
|
var offset = 9
|
||||||
for (i in 0 until deviceCount) {
|
for (i in 0 until deviceCount) {
|
||||||
if (offset + 8 > data.size) {
|
if (offset + 8 > data.size) {
|
||||||
Log.w(TAG, "Data array too short to parse all connected devices, returning what we have")
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Data array too short to parse all connected devices, returning what we have"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
val macBytes = data.sliceArray(offset until offset + 6)
|
val macBytes = data.sliceArray(offset until offset + 6)
|
||||||
@@ -1227,6 +1223,7 @@ class AACPManager {
|
|||||||
|
|
||||||
return devices
|
return devices
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendSomePacketIDontKnowWhatItIs() {
|
fun sendSomePacketIDontKnowWhatItIs() {
|
||||||
// 2900 00ff ffff ffff ffff -- enables setting EQ
|
// 2900 00ff ffff ffff ffff -- enables setting EQ
|
||||||
sendDataPacket(
|
sendDataPacket(
|
||||||
@@ -1264,7 +1261,7 @@ class AACPManager {
|
|||||||
val start = index
|
val start = index
|
||||||
// find next 0x00 byte
|
// find next 0x00 byte
|
||||||
while (index < data.size && data[index] != 0x00.toByte()) index++
|
while (index < data.size && data[index] != 0x00.toByte()) index++
|
||||||
val str = data.sliceArray(start until index).decodeToString()
|
val str = data.sliceArray(start..index).decodeToString()
|
||||||
strings.add(str)
|
strings.add(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
+8
-2
@@ -21,7 +21,7 @@
|
|||||||
* and receiving notifications. It is not a complete implementation of the ATT protocol.
|
* and receiving notifications. It is not a complete implementation of the ATT protocol.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.bluetooth
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -31,6 +31,7 @@ import android.os.ParcelUuid
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -62,7 +63,7 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
|
|||||||
private var input: InputStream? = null
|
private var input: InputStream? = null
|
||||||
private var output: OutputStream? = null
|
private var output: OutputStream? = null
|
||||||
private val listeners = mutableMapOf<Int, MutableList<(ByteArray) -> Unit>>()
|
private val listeners = mutableMapOf<Int, MutableList<(ByteArray) -> Unit>>()
|
||||||
private var notificationJob: kotlinx.coroutines.Job? = null
|
private var notificationJob: Job? = null
|
||||||
|
|
||||||
// queue for non-notification PDUs (responses to requests)
|
// queue for non-notification PDUs (responses to requests)
|
||||||
private val responses = LinkedBlockingQueue<ByteArray>()
|
private val responses = LinkedBlockingQueue<ByteArray>()
|
||||||
@@ -72,7 +73,12 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
|
|||||||
val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000")
|
val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000")
|
||||||
|
|
||||||
socket = createBluetoothSocket(adapter, device, uuid)
|
socket = createBluetoothSocket(adapter, device, uuid)
|
||||||
|
try {
|
||||||
socket!!.connect()
|
socket!!.connect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "ATT socket failed to connect")
|
||||||
|
return
|
||||||
|
}
|
||||||
input = socket!!.inputStream
|
input = socket!!.inputStream
|
||||||
output = socket!!.outputStream
|
output = socket!!.outputStream
|
||||||
Log.d(TAG, "Connected to ATT")
|
Log.d(TAG, "Connected to ATT")
|
||||||
+3
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.bluetooth
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
@@ -30,8 +30,10 @@ import android.content.SharedPreferences
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import me.kavishdevar.librepods.utils.BluetoothCryptography
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.collections.iterator
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.bluetooth
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
+1
-3
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@@ -256,8 +256,6 @@ data class AirPodsInstance(
|
|||||||
val version1: String?,
|
val version1: String?,
|
||||||
val version2: String?,
|
val version2: String?,
|
||||||
val version3: String?,
|
val version3: String?,
|
||||||
val aacpManager: AACPManager,
|
|
||||||
val attManager: ATTManager?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
object AirPodsModels {
|
object AirPodsModels {
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.data
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers
|
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
|
||||||
|
|
||||||
class ControlCommandRepository(
|
class ControlCommandRepository(
|
||||||
private val aacpManager: AACPManager
|
private val aacpManager: AACPManager
|
||||||
) {
|
) {
|
||||||
fun getValue(
|
fun getValue(
|
||||||
identifier: AACPManager.Companion.ControlCommandIdentifiers
|
identifier: ControlCommandIdentifiers
|
||||||
): ByteArray? {
|
): ByteArray? {
|
||||||
return aacpManager.controlCommandStatusList
|
return aacpManager.controlCommandStatusList
|
||||||
.find { it.identifier == identifier }
|
.find { it.identifier == identifier }
|
||||||
@@ -33,7 +33,7 @@ class ControlCommandRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setValue(
|
fun setValue(
|
||||||
id: AACPManager.Companion.ControlCommandIdentifiers,
|
id: ControlCommandIdentifiers,
|
||||||
value: ByteArray
|
value: ByteArray
|
||||||
) {
|
) {
|
||||||
aacpManager.sendControlCommand(id.value, value)
|
aacpManager.sendControlCommand(id.value, value)
|
||||||
@@ -41,7 +41,7 @@ class ControlCommandRepository(
|
|||||||
|
|
||||||
|
|
||||||
fun observe(
|
fun observe(
|
||||||
identifier: AACPManager.Companion.ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers,
|
||||||
onChange: (ByteArray) -> Unit
|
onChange: (ByteArray) -> Unit
|
||||||
): AACPManager.ControlCommandListener {
|
): AACPManager.ControlCommandListener {
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class ControlCommandRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun remove(
|
fun remove(
|
||||||
identifier: AACPManager.Companion.ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers,
|
||||||
listener: AACPManager.ControlCommandListener
|
listener: AACPManager.ControlCommandListener
|
||||||
) {
|
) {
|
||||||
aacpManager.unregisterControlCommandListener(identifier, listener)
|
aacpManager.unregisterControlCommandListener(identifier, listener)
|
||||||
|
|||||||
+3
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -25,6 +25,8 @@ 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.bluetooth.ATTHandles
|
||||||
|
import me.kavishdevar.librepods.bluetooth.ATTManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.constants
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
+2
-2
@@ -16,9 +16,9 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.constants
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
|
|
||||||
enum class StemAction {
|
enum class StemAction {
|
||||||
PLAY_PAUSE,
|
PLAY_PAUSE,
|
||||||
+2
-1
@@ -16,13 +16,14 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.bluetooth.ATTHandles
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
|
interface XposedRemotePref {
|
||||||
|
fun isAvailable(): Boolean
|
||||||
|
|
||||||
|
fun getBoolean(key: String, def: Boolean): Boolean
|
||||||
|
fun putBoolean(key: String, value: Boolean)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
|
object XposedRemotePrefProvider {
|
||||||
|
fun create(): XposedRemotePref = XposedRemotePrefImpl()
|
||||||
|
}
|
||||||
+3
-2
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -75,7 +75,8 @@ fun AboutCard(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+10
-6
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -35,6 +35,8 @@ 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.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.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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -61,7 +63,7 @@ fun AudioSettings(
|
|||||||
loudSoundReductionChecked: Boolean,
|
loudSoundReductionChecked: Boolean,
|
||||||
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
|
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
|
||||||
|
|
||||||
isXposed: Boolean,
|
vendorIdHook: Boolean,
|
||||||
isPremium: Boolean
|
isPremium: Boolean
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
@@ -80,7 +82,8 @@ fun AudioSettings(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -130,13 +133,14 @@ fun AudioSettings(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loudSoundReductionCapability && isXposed){
|
if (loudSoundReductionCapability && vendorIdHook){
|
||||||
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),
|
||||||
independent = false,
|
independent = false,
|
||||||
checked = loudSoundReductionChecked,
|
checked = loudSoundReductionChecked,
|
||||||
onCheckedChange = onLoudSoundReductionCheckedChange
|
onCheckedChange = onLoudSoundReductionCheckedChange,
|
||||||
|
enabled = isPremium
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -172,7 +176,7 @@ fun AudioSettingsPreview() {
|
|||||||
onConversationalAwarenessCheckedChange = { },
|
onConversationalAwarenessCheckedChange = { },
|
||||||
loudSoundReductionChecked = true,
|
loudSoundReductionChecked = true,
|
||||||
onLoudSoundReductionCheckedChange = { },
|
onLoudSoundReductionCheckedChange = { },
|
||||||
isXposed = true,
|
vendorIdHook = true,
|
||||||
isPremium = true
|
isPremium = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+50
-5
@@ -16,20 +16,21 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
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.padding
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -38,8 +39,10 @@ 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.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
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
|
||||||
@@ -48,11 +51,15 @@ 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 me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryIndicator(
|
fun BatteryIndicator(
|
||||||
batteryPercentage: Int,
|
batteryPercentage: Int,
|
||||||
charging: Boolean = false,
|
status: Int,
|
||||||
prefix: String = "",
|
prefix: String = "",
|
||||||
previousCharging: Boolean = false,
|
previousCharging: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -65,6 +72,7 @@ fun BatteryIndicator(
|
|||||||
|
|
||||||
val initialScale = if (previousCharging) 1f else 0f
|
val initialScale = if (previousCharging) 1f else 0f
|
||||||
val scaleAnim = remember { Animatable(initialScale) }
|
val scaleAnim = remember { Animatable(initialScale) }
|
||||||
|
val charging = status == BatteryStatus.CHARGING || status == BatteryStatus.OPTIMIZED_CHARGING
|
||||||
val targetScale = if (charging) 1f else 0f
|
val targetScale = if (charging) 1f else 0f
|
||||||
|
|
||||||
LaunchedEffect(previousCharging, charging) {
|
LaunchedEffect(previousCharging, charging) {
|
||||||
@@ -80,6 +88,43 @@ fun BatteryIndicator(
|
|||||||
modifier = Modifier.padding(bottom = 4.dp),
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
val strokeWidthPx = with(LocalDensity.current) { 4.dp.toPx() }
|
||||||
|
val gapFromCenterPx = with(LocalDensity.current) { 8.sp.toPx() }
|
||||||
|
|
||||||
|
if (status == BatteryStatus.OPTIMIZED_CHARGING) {
|
||||||
|
Canvas(modifier = Modifier.size(40.dp)) {
|
||||||
|
val radius = size.minDimension / 2
|
||||||
|
val progress = batteryPercentage / 100f
|
||||||
|
|
||||||
|
val angleDeg = -90f + 360f * progress
|
||||||
|
val angleRad = Math.toRadians(angleDeg.toDouble())
|
||||||
|
|
||||||
|
val outerX = center.x + (radius - strokeWidthPx) * cos(angleRad).toFloat()
|
||||||
|
val outerY = center.y + (radius - strokeWidthPx) * sin(angleRad).toFloat()
|
||||||
|
|
||||||
|
val dirX = center.x - outerX
|
||||||
|
val dirY = center.y - outerY
|
||||||
|
val length = sqrt(dirX * dirX + dirY * dirY)
|
||||||
|
|
||||||
|
val normX = dirX / length
|
||||||
|
val normY = dirY / length
|
||||||
|
|
||||||
|
val startX = outerX - normX * strokeWidthPx / 2
|
||||||
|
val startY = outerY - normY * strokeWidthPx / 2
|
||||||
|
|
||||||
|
val endX = center.x - normX * gapFromCenterPx
|
||||||
|
val endY = center.y - normY * gapFromCenterPx
|
||||||
|
|
||||||
|
drawLine(
|
||||||
|
color = batteryFillColor,
|
||||||
|
start = Offset(startX, startY),
|
||||||
|
end = Offset(endX, endY),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
cap = StrokeCap.Round
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
progress = { batteryPercentage / 100f },
|
progress = { batteryPercentage / 100f },
|
||||||
modifier = Modifier.size(40.dp),
|
modifier = Modifier.size(40.dp),
|
||||||
@@ -123,6 +168,6 @@ fun BatteryIndicatorPreview() {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.background(bg)
|
modifier = Modifier.background(bg)
|
||||||
) {
|
) {
|
||||||
BatteryIndicator(batteryPercentage = 24, charging = true, prefix = "\uDBC6\uDCE5", previousCharging = false)
|
BatteryIndicator(batteryPercentage = 80, status = BatteryStatus.CHARGING, prefix = "\uDBC6\uDCE5", previousCharging = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-18
@@ -18,10 +18,9 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
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
|
||||||
@@ -41,15 +40,14 @@ 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.LocalConfiguration
|
|
||||||
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.Battery
|
import me.kavishdevar.librepods.data.Battery
|
||||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
import me.kavishdevar.librepods.data.BatteryComponent
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -66,12 +64,6 @@ fun BatteryView(
|
|||||||
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 ||
|
val caseCharging = case?.status == BatteryStatus.CHARGING ||
|
||||||
case?.status == BatteryStatus.OPTIMIZED_CHARGING
|
case?.status == BatteryStatus.OPTIMIZED_CHARGING
|
||||||
|
|
||||||
@@ -98,12 +90,12 @@ fun BatteryView(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
leftCharging == rightCharging &&
|
left?.status == right?.status &&
|
||||||
(leftLevel - rightLevel) in -3..3
|
(leftLevel - rightLevel) in -3..3
|
||||||
) {
|
) {
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
leftLevel.coerceAtMost(rightLevel),
|
leftLevel.coerceAtMost(rightLevel),
|
||||||
leftCharging
|
left?.status ?: BatteryStatus.NOT_CHARGING
|
||||||
)
|
)
|
||||||
singleDisplayed.value = true
|
singleDisplayed.value = true
|
||||||
} else {
|
} else {
|
||||||
@@ -116,7 +108,7 @@ fun BatteryView(
|
|||||||
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
|
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
leftLevel,
|
leftLevel,
|
||||||
leftCharging,
|
left?.status ?: BatteryStatus.NOT_CHARGING,
|
||||||
"\uDBC6\uDCE5"
|
"\uDBC6\uDCE5"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -128,7 +120,7 @@ fun BatteryView(
|
|||||||
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
|
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
rightLevel,
|
rightLevel,
|
||||||
rightCharging,
|
right?.status ?: BatteryStatus.NOT_CHARGING,
|
||||||
"\uDBC6\uDCE8"
|
"\uDBC6\uDCE8"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -151,7 +143,7 @@ fun BatteryView(
|
|||||||
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
|
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
caseLevel,
|
caseLevel,
|
||||||
caseCharging,
|
case?.status ?: BatteryStatus.NOT_CHARGING,
|
||||||
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
|
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -165,7 +157,7 @@ fun BatteryView(
|
|||||||
fun BatteryViewPreview() {
|
fun BatteryViewPreview() {
|
||||||
val fakeBattery = listOf(
|
val fakeBattery = listOf(
|
||||||
Battery(BatteryComponent.LEFT, 85, BatteryStatus.CHARGING),
|
Battery(BatteryComponent.LEFT, 85, BatteryStatus.CHARGING),
|
||||||
Battery(BatteryComponent.RIGHT, 40, BatteryStatus.CHARGING),
|
Battery(BatteryComponent.RIGHT, 40, BatteryStatus.OPTIMIZED_CHARGING),
|
||||||
Battery(BatteryComponent.CASE, 60, BatteryStatus.NOT_CHARGING)
|
Battery(BatteryComponent.CASE, 60, BatteryStatus.NOT_CHARGING)
|
||||||
)
|
)
|
||||||
|
|
||||||
+28
-3
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -41,15 +41,18 @@ 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
|
||||||
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
|
||||||
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.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInParent
|
import androidx.compose.ui.layout.positionInParent
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -59,6 +62,7 @@ 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 kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -82,7 +86,8 @@ fun CallControlSettings(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -93,6 +98,10 @@ fun CallControlSettings(
|
|||||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -105,6 +114,7 @@ fun CallControlSettings(
|
|||||||
var lastDismissTimeSingle by remember { mutableLongStateOf(0L) }
|
var lastDismissTimeSingle by remember { mutableLongStateOf(0L) }
|
||||||
var parentHoveredIndexSingle by remember { mutableStateOf<Int?>(null) }
|
var parentHoveredIndexSingle by remember { mutableStateOf<Int?>(null) }
|
||||||
var parentDragActiveSingle by remember { mutableStateOf(false) }
|
var parentDragActiveSingle by remember { mutableStateOf(false) }
|
||||||
|
var previousIdxSingle by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
var showDoublePressDropdown by remember { mutableStateOf(false) }
|
var showDoublePressDropdown by remember { mutableStateOf(false) }
|
||||||
var touchOffsetDouble by remember { mutableStateOf<Offset?>(null) }
|
var touchOffsetDouble by remember { mutableStateOf<Offset?>(null) }
|
||||||
@@ -112,6 +122,7 @@ fun CallControlSettings(
|
|||||||
var lastDismissTimeDouble by remember { mutableLongStateOf(0L) }
|
var lastDismissTimeDouble by remember { mutableLongStateOf(0L) }
|
||||||
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) }
|
||||||
|
var previousIdxDouble by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(flipped) {
|
LaunchedEffect(flipped) {
|
||||||
Log.d("CallControlSettings", "Call control flipped: $flipped")
|
Log.d("CallControlSettings", "Call control flipped: $flipped")
|
||||||
@@ -187,7 +198,11 @@ fun CallControlSettings(
|
|||||||
val touch = touchOffsetSingle ?: current
|
val touch = touchOffsetSingle ?: current
|
||||||
val posInPopupY = current.y - touch.y
|
val posInPopupY = current.y - touch.y
|
||||||
val idx = (posInPopupY / itemHeightPx).toInt()
|
val idx = (posInPopupY / itemHeightPx).toInt()
|
||||||
|
if (idx != previousIdxSingle) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
|
||||||
|
}
|
||||||
parentHoveredIndexSingle = idx
|
parentHoveredIndexSingle = idx
|
||||||
|
previousIdxSingle = idx
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
parentDragActiveSingle = false
|
parentDragActiveSingle = false
|
||||||
@@ -204,6 +219,9 @@ fun CallControlSettings(
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (parentHoveredIndexSingle != null && parentHoveredIndexSingle in 0..1) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
|
||||||
|
}
|
||||||
parentHoveredIndexSingle = null
|
parentHoveredIndexSingle = null
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
@@ -316,7 +334,11 @@ fun CallControlSettings(
|
|||||||
val touch = touchOffsetDouble ?: current
|
val touch = touchOffsetDouble ?: current
|
||||||
val posInPopupY = current.y - touch.y
|
val posInPopupY = current.y - touch.y
|
||||||
val idx = (posInPopupY / itemHeightPx).toInt()
|
val idx = (posInPopupY / itemHeightPx).toInt()
|
||||||
|
if (idx != previousIdxDouble) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
|
||||||
|
}
|
||||||
parentHoveredIndexDouble = idx
|
parentHoveredIndexDouble = idx
|
||||||
|
previousIdxDouble = idx
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
parentDragActiveDouble = false
|
parentDragActiveDouble = false
|
||||||
@@ -330,9 +352,12 @@ fun CallControlSettings(
|
|||||||
showDoublePressDropdown = false
|
showDoublePressDropdown = false
|
||||||
lastDismissTimeDouble = System.currentTimeMillis()
|
lastDismissTimeDouble = System.currentTimeMillis()
|
||||||
val flipped = option == pressOnceText
|
val flipped = option == pressOnceText
|
||||||
onCallControlValueChanged (flipped)
|
onCallControlValueChanged(flipped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (parentHoveredIndexDouble != null && parentHoveredIndexDouble in 0..1) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
|
||||||
|
}
|
||||||
parentHoveredIndexDouble = null
|
parentHoveredIndexDouble = null
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
+27
-5
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -24,6 +24,7 @@ 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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -38,13 +39,16 @@ 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.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
|
||||||
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.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -53,10 +57,12 @@ 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.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -74,8 +80,18 @@ fun ConfirmationDialog(
|
|||||||
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 accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
|
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (showDialog.value) {
|
if (showDialog.value) {
|
||||||
Dialog(onDismissRequest = { showDialog.value = false }) {
|
Dialog(
|
||||||
|
onDismissRequest = { showDialog.value = false },
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = false,
|
||||||
|
dismissOnClickOutside = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// .fillMaxWidth(0.75f)
|
// .fillMaxWidth(0.75f)
|
||||||
@@ -90,7 +106,7 @@ fun ConfirmationDialog(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -102,7 +118,7 @@ fun ConfirmationDialog(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
message,
|
message,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -113,7 +129,7 @@ fun ConfirmationDialog(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
@@ -148,6 +164,8 @@ fun ConfirmationDialog(
|
|||||||
}
|
}
|
||||||
PointerEventType.Move -> {
|
PointerEventType.Move -> {
|
||||||
if (isWithinBounds) {
|
if (isWithinBounds) {
|
||||||
|
if (leftPressed != isLeft) scope.launch { haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.SegmentTick) }
|
||||||
leftPressed = isLeft
|
leftPressed = isLeft
|
||||||
rightPressed = !isLeft
|
rightPressed = !isLeft
|
||||||
} else {
|
} else {
|
||||||
@@ -158,8 +176,12 @@ fun ConfirmationDialog(
|
|||||||
PointerEventType.Release -> {
|
PointerEventType.Release -> {
|
||||||
if (isWithinBounds) {
|
if (isWithinBounds) {
|
||||||
if (leftPressed) {
|
if (leftPressed) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.Reject) }
|
||||||
onDismiss()
|
onDismiss()
|
||||||
} else if (rightPressed) {
|
} else if (rightPressed) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.Confirm) }
|
||||||
onConfirm()
|
onConfirm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:Suppress("unused")
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -56,7 +56,7 @@ 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 me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
import me.kavishdevar.librepods.data.NoiseControlMode
|
||||||
|
|
||||||
private val ContainerColor = Color(0x593C3C3E)
|
private val ContainerColor = Color(0x593C3C3E)
|
||||||
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
|
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
|
||||||
+7
-4
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -35,6 +35,8 @@ 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.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.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
|
||||||
@@ -47,12 +49,12 @@ fun HearingHealthSettings(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
hasPPECapability: Boolean,
|
hasPPECapability: Boolean,
|
||||||
hasHearingAidCapability: Boolean,
|
hasHearingAidCapability: Boolean,
|
||||||
isXposed: Boolean
|
vendorIdHook: 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 backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val shouldShowHearingAid = hasHearingAidCapability && isXposed
|
val shouldShowHearingAid = hasHearingAidCapability && vendorIdHook
|
||||||
|
|
||||||
if (hasPPECapability && shouldShowHearingAid) {
|
if (hasPPECapability && shouldShowHearingAid) {
|
||||||
Box(
|
Box(
|
||||||
@@ -65,7 +67,8 @@ fun HearingHealthSettings(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+15
-19
@@ -18,9 +18,8 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
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,15 +38,18 @@ 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
|
||||||
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
|
||||||
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.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInParent
|
import androidx.compose.ui.layout.positionInParent
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -56,8 +58,8 @@ 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 kotlinx.coroutines.launch
|
||||||
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
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -93,26 +95,13 @@ fun MicrophoneSettings(
|
|||||||
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
||||||
val reopenThresholdMs = 250L
|
val reopenThresholdMs = 250L
|
||||||
|
|
||||||
val listener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (AACPManager.Companion.ControlCommandIdentifiers.fromByte(controlCommand.identifier) ==
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE
|
|
||||||
) {
|
|
||||||
selectedMode = when (controlCommand.value[0]) {
|
|
||||||
0x00.toByte() -> "Automatic"
|
|
||||||
0x01.toByte() -> "Always Right"
|
|
||||||
0x02.toByte() -> "Always Left"
|
|
||||||
else -> "Automatic"
|
|
||||||
}
|
|
||||||
Log.d("MicrophoneSettings", "Microphone mode received: $selectedMode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) }
|
||||||
var parentDragActive by remember { mutableStateOf(false) }
|
var parentDragActive by remember { mutableStateOf(false) }
|
||||||
|
var previousIdx by remember { mutableStateOf<Int?>(null) }
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val microphoneAutomaticText = stringResource(R.string.microphone_automatic)
|
val microphoneAutomaticText = stringResource(R.string.microphone_automatic)
|
||||||
val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right)
|
val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right)
|
||||||
val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left)
|
val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left)
|
||||||
@@ -152,7 +141,11 @@ fun MicrophoneSettings(
|
|||||||
val touch = touchOffset ?: current
|
val touch = touchOffset ?: current
|
||||||
val posInPopupY = current.y - touch.y
|
val posInPopupY = current.y - touch.y
|
||||||
val idx = (posInPopupY / itemHeightPx).toInt()
|
val idx = (posInPopupY / itemHeightPx).toInt()
|
||||||
|
if (idx != previousIdx) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
|
||||||
|
}
|
||||||
parentHoveredIndex = idx
|
parentHoveredIndex = idx
|
||||||
|
previousIdx = idx
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
parentDragActive = false
|
parentDragActive = false
|
||||||
@@ -180,6 +173,9 @@ fun MicrophoneSettings(
|
|||||||
onMicModeValueChanged(byteValue.toByte())
|
onMicModeValueChanged(byteValue.toByte())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (parentHoveredIndex != null && parentHoveredIndex in 0..2) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
|
||||||
|
}
|
||||||
parentHoveredIndex = null
|
parentHoveredIndex = null
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
+24
-5
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -35,21 +35,25 @@ import androidx.compose.runtime.Composable
|
|||||||
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
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
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 kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -62,10 +66,14 @@ fun NavigationButton(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
currentState: String? = null,
|
currentState: String? = null,
|
||||||
height: Dp = 58.dp,
|
height: Dp = 58.dp,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
Box(
|
Box(
|
||||||
@@ -79,24 +87,35 @@ fun NavigationButton(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f),
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(if (independent) 28.dp else 0.dp))
|
.background(
|
||||||
|
animatedBackgroundColor,
|
||||||
|
RoundedCornerShape(if (independent) 28.dp else 0.dp)
|
||||||
|
)
|
||||||
.height(height)
|
.height(height)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
if (enabled) {
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
tryAwaitRelease()
|
tryAwaitRelease()
|
||||||
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
|
if (enabled) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
||||||
if (onClick != null) onClick() else navController.navigate(to)
|
if (onClick != null) onClick() else navController.navigate(to)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
+5
-2
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
@@ -59,6 +59,8 @@ 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.Font
|
||||||
|
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.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
@@ -66,7 +68,7 @@ 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.NoiseControlMode
|
import me.kavishdevar.librepods.data.NoiseControlMode
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -147,6 +149,7 @@ fun NoiseControlSettings(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -40,7 +40,7 @@ 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.constants.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PressAndHoldSettings(
|
fun PressAndHoldSettings(
|
||||||
+42
-13
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.graphics.RuntimeShader
|
import android.graphics.RuntimeShader
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -46,7 +46,9 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.ShaderBrush
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
import androidx.compose.ui.graphics.isSpecified
|
import androidx.compose.ui.graphics.isSpecified
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastCoerceAtMost
|
import androidx.compose.ui.util.fastCoerceAtMost
|
||||||
@@ -77,7 +79,8 @@ fun StyledButton(
|
|||||||
maxScale: Float = 0.1f,
|
maxScale: Float = 0.1f,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val animationScope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
val progressAnimation = remember { Animatable(0f) }
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
|
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
|
||||||
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
|
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
|
||||||
@@ -163,8 +166,10 @@ half4 main(float2 coord) {
|
|||||||
val maxOffset = size.minDimension
|
val maxOffset = size.minDimension
|
||||||
val initialDerivative = 0.05f
|
val initialDerivative = 0.05f
|
||||||
val offset = offsetAnimation.value
|
val offset = offsetAnimation.value
|
||||||
translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset)
|
translationX =
|
||||||
translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset)
|
maxOffset * tanh(initialDerivative * offset.x / maxOffset)
|
||||||
|
translationY =
|
||||||
|
maxOffset * tanh(initialDerivative * offset.y / maxOffset)
|
||||||
|
|
||||||
val maxDragScale = 0.1f
|
val maxDragScale = 0.1f
|
||||||
val offsetAngle = atan2(offset.y, offset.x)
|
val offsetAngle = atan2(offset.y, offset.x)
|
||||||
@@ -209,7 +214,10 @@ half4 main(float2 coord) {
|
|||||||
interactiveHighlightShader.apply {
|
interactiveHighlightShader.apply {
|
||||||
val offset = pressStartPosition + offsetAnimation.value
|
val offset = pressStartPosition + offsetAnimation.value
|
||||||
setFloatUniform("size", size.width, size.height)
|
setFloatUniform("size", size.width, size.height)
|
||||||
setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
|
setColorUniform(
|
||||||
|
"color",
|
||||||
|
Color.White.copy(0.15f * progress).toArgb()
|
||||||
|
)
|
||||||
setFloatUniform("radius", size.maxDimension)
|
setFloatUniform("radius", size.maxDimension)
|
||||||
setFloatUniform(
|
setFloatUniform(
|
||||||
"offset",
|
"offset",
|
||||||
@@ -236,31 +244,51 @@ half4 main(float2 coord) {
|
|||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
indication = null,
|
indication = null,
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
onClick = onClick
|
onClick = {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
if (isInteractive) {
|
if (isInteractive) {
|
||||||
Modifier.pointerInput(animationScope) {
|
Modifier.pointerInput(scope) {
|
||||||
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)
|
||||||
val onDragStop: () -> Unit = {
|
val onDragStop: () -> Unit = {
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
|
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
||||||
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
launch {
|
||||||
|
offsetAnimation.animateTo(
|
||||||
|
Offset.Zero,
|
||||||
|
offsetAnimationSpec
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inspectDragGestures(
|
inspectDragGestures(
|
||||||
onDragStart = { down ->
|
onDragStart = { down ->
|
||||||
pressStartPosition = down.position
|
pressStartPosition = down.position
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
||||||
|
launch {
|
||||||
|
progressAnimation.animateTo(
|
||||||
|
1f,
|
||||||
|
progressAnimationSpec
|
||||||
|
)
|
||||||
|
}
|
||||||
launch { offsetAnimation.snapTo(Offset.Zero) }
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragEnd = { onDragStop() },
|
onDragEnd = {
|
||||||
|
onDragStop()
|
||||||
|
},
|
||||||
onDragCancel = onDragStop
|
onDragCancel = onDragStop
|
||||||
) { _, dragAmount ->
|
) { _, dragAmount ->
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
|
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.SegmentFrequentTick
|
||||||
|
)
|
||||||
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,6 +302,7 @@ half4 main(float2 coord) {
|
|||||||
isPressed = false
|
isPressed = false
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
+17
-2
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -49,14 +49,17 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
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
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
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.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -71,6 +74,7 @@ import dev.chrisbanes.haze.HazeTint
|
|||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -110,6 +114,9 @@ fun StyledDropdown(
|
|||||||
var hoveredIndex by remember { mutableStateOf<Int?>(null) }
|
var hoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
val itemHeight = 48.dp
|
val itemHeight = 48.dp
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
|
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
|
||||||
var lastDragPosition by remember { mutableStateOf<Offset?>(null) }
|
var lastDragPosition by remember { mutableStateOf<Offset?>(null) }
|
||||||
|
|
||||||
@@ -132,7 +139,12 @@ fun StyledDropdown(
|
|||||||
},
|
},
|
||||||
onDrag = { change, _ ->
|
onDrag = { change, _ ->
|
||||||
val y = change.position.y
|
val y = change.position.y
|
||||||
hoveredIndex = (y / itemHeight.toPx()).toInt()
|
val newHoveredIndex = (y / itemHeight.toPx()).toInt()
|
||||||
|
if (newHoveredIndex != hoveredIndex) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.SegmentTick) }
|
||||||
|
}
|
||||||
|
hoveredIndex = newHoveredIndex
|
||||||
lastDragPosition = change.position
|
lastDragPosition = change.position
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
@@ -144,6 +156,8 @@ fun StyledDropdown(
|
|||||||
if (withinBounds) {
|
if (withinBounds) {
|
||||||
hoveredIndex?.let { idx ->
|
hoveredIndex?.let { idx ->
|
||||||
if (idx in options.indices) {
|
if (idx in options.indices) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.GestureEnd) }
|
||||||
onOptionSelected(options[idx])
|
onOptionSelected(options[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +188,7 @@ fun StyledDropdown(
|
|||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null
|
indication = null
|
||||||
) {
|
) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
||||||
onOptionSelected(text)
|
onOptionSelected(text)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}
|
}
|
||||||
+28
-9
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.graphics.RuntimeShader
|
import android.graphics.RuntimeShader
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -50,7 +50,9 @@ import androidx.compose.ui.graphics.layer.CompositingStrategy
|
|||||||
import androidx.compose.ui.graphics.layer.drawLayer
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -83,8 +85,9 @@ fun StyledIconButton(
|
|||||||
backdrop: LayerBackdrop = rememberLayerBackdrop(),
|
backdrop: LayerBackdrop = rememberLayerBackdrop(),
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
val darkMode = isSystemInDarkTheme()
|
val darkMode = isSystemInDarkTheme()
|
||||||
val animationScope = rememberCoroutineScope()
|
val scope = 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)
|
||||||
val progressAnimation = remember { Animatable(0f) }
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
@@ -116,7 +119,10 @@ half4 main(float2 coord) {
|
|||||||
}
|
}
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onClick,
|
onClick = {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(56.dp),
|
shape = RoundedCornerShape(56.dp),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
@@ -182,7 +188,12 @@ half4 main(float2 coord) {
|
|||||||
drawLayer(innerShadowLayer)
|
drawLayer(innerShadowLayer)
|
||||||
|
|
||||||
drawRect(
|
drawRect(
|
||||||
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(progress.coerceIn(0.15f, 0.35f))
|
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(
|
||||||
|
progress.coerceIn(
|
||||||
|
0.15f,
|
||||||
|
0.35f
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDrawFront = {
|
onDrawFront = {
|
||||||
@@ -196,7 +207,10 @@ half4 main(float2 coord) {
|
|||||||
interactiveHighlightShader.apply {
|
interactiveHighlightShader.apply {
|
||||||
val offset = pressStartPosition + offsetAnimation.value
|
val offset = pressStartPosition + offsetAnimation.value
|
||||||
setFloatUniform("size", size.width, size.height)
|
setFloatUniform("size", size.width, size.height)
|
||||||
setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
|
setColorUniform(
|
||||||
|
"color",
|
||||||
|
Color.White.copy(0.15f * progress).toArgb()
|
||||||
|
)
|
||||||
setFloatUniform("radius", size.maxDimension)
|
setFloatUniform("radius", size.maxDimension)
|
||||||
setFloatUniform(
|
setFloatUniform(
|
||||||
"offset",
|
"offset",
|
||||||
@@ -225,9 +239,10 @@ half4 main(float2 coord) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.pointerInput(animationScope) {
|
.pointerInput(scope) {
|
||||||
val onDragStop: () -> Unit = {
|
val onDragStop: () -> Unit = {
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
|
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
||||||
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
||||||
}
|
}
|
||||||
@@ -235,7 +250,8 @@ half4 main(float2 coord) {
|
|||||||
inspectDragGestures(
|
inspectDragGestures(
|
||||||
onDragStart = { down ->
|
onDragStart = { down ->
|
||||||
pressStartPosition = down.position
|
pressStartPosition = down.position
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
|
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
||||||
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
||||||
launch { offsetAnimation.snapTo(Offset.Zero) }
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
}
|
}
|
||||||
@@ -243,7 +259,10 @@ half4 main(float2 coord) {
|
|||||||
onDragEnd = { onDragStop() },
|
onDragEnd = { onDragStop() },
|
||||||
onDragCancel = onDragStop
|
onDragCancel = onDragStop
|
||||||
) { _, dragAmount ->
|
) { _, dragAmount ->
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
|
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
||||||
|
HapticFeedbackType.SegmentFrequentTick
|
||||||
|
)
|
||||||
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
+12
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -39,11 +39,14 @@ import androidx.compose.runtime.Composable
|
|||||||
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
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.painterResource
|
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
|
||||||
@@ -71,6 +74,9 @@ fun StyledSelectList(
|
|||||||
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 scope = rememberCoroutineScope()
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -104,6 +110,11 @@ fun StyledSelectList(
|
|||||||
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
tryAwaitRelease()
|
tryAwaitRelease()
|
||||||
itemBackgroundColor = backgroundColor
|
itemBackgroundColor = backgroundColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
if (item.enabled) {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
|
||||||
item.onClick()
|
item.onClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
-4
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
@@ -56,6 +56,7 @@ 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.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||||
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
||||||
@@ -64,6 +65,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.layout.positionInParent
|
import androidx.compose.ui.layout.positionInParent
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -246,6 +248,8 @@ fun StyledSlider(
|
|||||||
val startIconWidthState = remember { mutableFloatStateOf(0f) }
|
val startIconWidthState = remember { mutableFloatStateOf(0f) }
|
||||||
val endIconWidthState = remember { mutableFloatStateOf(0f) }
|
val endIconWidthState = remember { mutableFloatStateOf(0f) }
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
var lastDragValue by remember { mutableFloatStateOf(value) }
|
||||||
|
|
||||||
val momentumAnimation = rememberMomentumAnimation(maxScale = 1.5f)
|
val momentumAnimation = rememberMomentumAnimation(maxScale = 1.5f)
|
||||||
|
|
||||||
@@ -449,10 +453,17 @@ fun StyledSlider(
|
|||||||
valueRange.start,
|
valueRange.start,
|
||||||
valueRange.endInclusive
|
valueRange.endInclusive
|
||||||
)
|
)
|
||||||
|
snapPoints.forEach { snap ->
|
||||||
|
if ((lastDragValue < snap && targetValue >= snap) ||
|
||||||
|
(snap in targetValue..<lastDragValue)) {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.SegmentTick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastDragValue = targetValue
|
||||||
val snappedValue = if (snapPoints.isNotEmpty()) snapIfClose(
|
val snappedValue = if (snapPoints.isNotEmpty()) snapIfClose(
|
||||||
targetValue,
|
targetValue,
|
||||||
snapPoints,
|
snapPoints,
|
||||||
snapThreshold
|
snapThreshold,
|
||||||
) else targetValue
|
) else targetValue
|
||||||
onValueChange(snappedValue)
|
onValueChange(snappedValue)
|
||||||
}
|
}
|
||||||
@@ -460,10 +471,9 @@ fun StyledSlider(
|
|||||||
Orientation.Horizontal,
|
Orientation.Horizontal,
|
||||||
startDragImmediately = true,
|
startDragImmediately = true,
|
||||||
onDragStarted = {
|
onDragStarted = {
|
||||||
// Remove this block as momentumAnimation handles pressing
|
lastDragValue = value
|
||||||
},
|
},
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
// Remove this block as momentumAnimation handles pressing
|
|
||||||
onValueChange((value * 100).roundToInt() / 100f)
|
onValueChange((value * 100).roundToInt() / 100f)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
+22
-7
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
@@ -58,8 +58,10 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
import androidx.compose.ui.graphics.layer.drawLayer
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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.util.fastCoerceIn
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
@@ -73,6 +75,7 @@ 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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSwitch(
|
fun StyledSwitch(
|
||||||
@@ -81,9 +84,12 @@ fun StyledSwitch(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
val onColor = if (enabled) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
val onColor = if (enabled) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
||||||
val offColor = if (enabled) if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
val offColor = if (enabled) if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) else if (isDarkTheme) Color(
|
||||||
|
0x805B5B5E
|
||||||
|
) else Color(0xFFD1D1D6)
|
||||||
|
|
||||||
val trackWidth = 64.dp
|
val trackWidth = 64.dp
|
||||||
val trackHeight = 28.dp
|
val trackHeight = 28.dp
|
||||||
@@ -98,7 +104,7 @@ fun StyledSwitch(
|
|||||||
val animatedFraction = remember { Animatable(fraction) }
|
val animatedFraction = remember { Animatable(fraction) }
|
||||||
val trackWidthPx = remember { mutableFloatStateOf(0f) }
|
val trackWidthPx = remember { mutableFloatStateOf(0f) }
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val animationScope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
val progressAnimation = remember { Animatable(0f) }
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
val innerShadowLayer = rememberGraphicsLayer().apply {
|
val innerShadowLayer = rememberGraphicsLayer().apply {
|
||||||
@@ -111,6 +117,11 @@ fun StyledSwitch(
|
|||||||
val isFirstComposition = remember { mutableStateOf(true) }
|
val isFirstComposition = remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(checked) {
|
LaunchedEffect(checked) {
|
||||||
if (!isFirstComposition.value) {
|
if (!isFirstComposition.value) {
|
||||||
|
if (checked) {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.ToggleOn)
|
||||||
|
} else {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.ToggleOff)
|
||||||
|
}
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch {
|
launch {
|
||||||
val targetFrac = if (checked) 1f else 0f
|
val targetFrac = if (checked) 1f else 0f
|
||||||
@@ -150,27 +161,31 @@ fun StyledSwitch(
|
|||||||
.then(if (enabled) Modifier.draggable(
|
.then(if (enabled) Modifier.draggable(
|
||||||
rememberDraggableState { delta ->
|
rememberDraggableState { delta ->
|
||||||
if (trackWidthPx.floatValue > 0f) {
|
if (trackWidthPx.floatValue > 0f) {
|
||||||
|
val oldFraction = animatedFraction.value
|
||||||
val newFraction = (animatedFraction.value + delta / trackWidthPx.floatValue).fastCoerceIn(-0.3f, 1.3f)
|
val newFraction = (animatedFraction.value + delta / trackWidthPx.floatValue).fastCoerceIn(-0.3f, 1.3f)
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
animatedFraction.snapTo(newFraction)
|
animatedFraction.snapTo(newFraction)
|
||||||
}
|
}
|
||||||
totalDrag.floatValue += kotlin.math.abs(delta)
|
totalDrag.floatValue += abs(delta)
|
||||||
val newChecked = newFraction >= 0.5f
|
val newChecked = newFraction >= 0.5f
|
||||||
if (newChecked != checked) {
|
if (newChecked != checked) {
|
||||||
onCheckedChange(newChecked)
|
onCheckedChange(newChecked)
|
||||||
}
|
}
|
||||||
|
if ((oldFraction < 0.5f && newFraction >= 0.5f) || (oldFraction >= 0.5f && newFraction < 0.5f)) {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.SegmentTick)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Orientation.Horizontal,
|
Orientation.Horizontal,
|
||||||
startDragImmediately = true,
|
startDragImmediately = true,
|
||||||
onDragStarted = {
|
onDragStarted = {
|
||||||
totalDrag.floatValue = 0f
|
totalDrag.floatValue = 0f
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
progressAnimation.animateTo(1f, progressAnimationSpec)
|
progressAnimation.animateTo(1f, progressAnimationSpec)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
animationScope.launch {
|
scope.launch {
|
||||||
if (totalDrag.floatValue < tapThreshold) {
|
if (totalDrag.floatValue < tapThreshold) {
|
||||||
val newChecked = !checked
|
val newChecked = !checked
|
||||||
onCheckedChange(newChecked)
|
onCheckedChange(newChecked)
|
||||||
+15
-2
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -40,12 +40,15 @@ import androidx.compose.runtime.Composable
|
|||||||
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.rememberUpdatedState
|
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.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -53,6 +56,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 kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -71,6 +75,9 @@ fun StyledToggle(
|
|||||||
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 haptics = LocalHapticFeedback.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var backgroundColor by remember {
|
var backgroundColor by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
@@ -90,7 +97,8 @@ fun StyledToggle(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
start = 16.dp,
|
start = 16.dp,
|
||||||
@@ -108,14 +116,17 @@ fun StyledToggle(
|
|||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
|
if (enabled) {
|
||||||
backgroundColor =
|
backgroundColor =
|
||||||
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
tryAwaitRelease()
|
tryAwaitRelease()
|
||||||
backgroundColor =
|
backgroundColor =
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(if (!currentChecked) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) }
|
||||||
onCheckedChange(!currentChecked)
|
onCheckedChange(!currentChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +156,7 @@ fun StyledToggle(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(if (it) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) }
|
||||||
onCheckedChange(it)
|
onCheckedChange(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,6 +212,7 @@ fun StyledToggle(
|
|||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(if (!currentChecked) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) }
|
||||||
onCheckedChange(!currentChecked)
|
onCheckedChange(!currentChecked)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
+5
-5
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.presentation.overlays
|
||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
@@ -58,10 +58,10 @@ import androidx.dynamicanimation.animation.DynamicAnimation
|
|||||||
import androidx.dynamicanimation.animation.SpringAnimation
|
import androidx.dynamicanimation.animation.SpringAnimation
|
||||||
import androidx.dynamicanimation.animation.SpringForce
|
import androidx.dynamicanimation.animation.SpringForce
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.constants.Battery
|
import me.kavishdevar.librepods.data.Battery
|
||||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
import me.kavishdevar.librepods.data.BatteryComponent
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
+5
-5
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.presentation.overlays
|
||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
@@ -45,10 +45,10 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.VideoView
|
import android.widget.VideoView
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.constants.Battery
|
import me.kavishdevar.librepods.data.Battery
|
||||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
import me.kavishdevar.librepods.data.BatteryComponent
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
|
|
||||||
@SuppressLint("InflateParams", "ClickableViewAccessibility")
|
@SuppressLint("InflateParams", "ClickableViewAccessibility")
|
||||||
class PopupWindow(
|
class PopupWindow(
|
||||||
+114
-26
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -44,16 +44,20 @@ 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
|
||||||
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
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
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.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInParent
|
import androidx.compose.ui.layout.positionInParent
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
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
|
||||||
@@ -67,17 +71,18 @@ 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 me.kavishdevar.librepods.BuildConfig
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.composables.StyledDropdown
|
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.data.Capability
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.presentation.components.StyledDropdown
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.Capability
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
//private var phoneMediaDebounceJob: Job? = null
|
//private var phoneMediaDebounceJob: Job? = null
|
||||||
@@ -96,7 +101,6 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
|
|
||||||
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 hearingAidEnabled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(1)?.toInt() == 1 && state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0)?.toInt() == 1
|
||||||
|
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
@@ -113,6 +117,28 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(topPadding))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
|
||||||
|
if (!state.isPremium) {
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("purchase_screen")
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.unlock_advanced_features),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = Color.White
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) }
|
||||||
@@ -179,12 +205,26 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
Box (
|
||||||
|
modifier = Modifier.then(
|
||||||
|
if (!state.isPremium) {
|
||||||
|
Modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||||
|
event.changes.forEach { it.consume() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = stringResource(R.string.press_speed),
|
label = stringResource(R.string.press_speed),
|
||||||
description = stringResource(R.string.press_speed_description),
|
description = stringResource(R.string.press_speed_description),
|
||||||
options = pressSpeedOptions.values.toList(),
|
options = pressSpeedOptions.values.toList(),
|
||||||
selectedOption = selectedPressSpeed?: stringResource(R.string.default_option),
|
selectedOption = selectedPressSpeed ?: stringResource(R.string.default_option),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedPressSpeed = newValue
|
selectedPressSpeed = newValue
|
||||||
viewModel.setControlCommandByte(
|
viewModel.setControlCommandByte(
|
||||||
@@ -197,12 +237,29 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
hazeState = hazeState,
|
hazeState = hazeState,
|
||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box (
|
||||||
|
modifier = Modifier.then(
|
||||||
|
if (!state.isPremium) {
|
||||||
|
Modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||||
|
event.changes.forEach { it.consume() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = stringResource(R.string.press_and_hold_duration),
|
label = stringResource(R.string.press_and_hold_duration),
|
||||||
description = stringResource(R.string.press_and_hold_duration_description),
|
description = stringResource(R.string.press_and_hold_duration_description),
|
||||||
options = pressAndHoldDurationOptions.values.toList(),
|
options = pressAndHoldDurationOptions.values.toList(),
|
||||||
selectedOption = selectedPressAndHoldDuration?: stringResource(R.string.default_option),
|
selectedOption = selectedPressAndHoldDuration
|
||||||
|
?: stringResource(R.string.default_option),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedPressAndHoldDuration = newValue
|
selectedPressAndHoldDuration = newValue
|
||||||
viewModel.setControlCommandByte(
|
viewModel.setControlCommandByte(
|
||||||
@@ -215,30 +272,33 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
hazeState = hazeState,
|
hazeState = hazeState,
|
||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
|
}
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
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),
|
||||||
independent = true,
|
independent = true,
|
||||||
checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE]?.getOrNull(0) == 0x01.toByte(),
|
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) }
|
onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it) },
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
if (state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && BuildConfig.FLAVOR == "xposed") {
|
if (state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && state.vendorIdHook) {
|
||||||
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),
|
||||||
checked = viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)?.get(0) == 1.toByte(),
|
checked = state.loudSoundReductionEnabled,
|
||||||
onCheckedChange = { viewModel.setATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION, if (it) byteArrayOf(0x01) else byteArrayOf(0x00)) }
|
onCheckedChange = { viewModel.setATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION, if (it) byteArrayOf(0x01) else byteArrayOf(0x00)) },
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") {
|
if (!hearingAidEnabled && state.vendorIdHook) {
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "transparency_customization",
|
to = "transparency_customization",
|
||||||
name = stringResource(R.string.customize_transparency_mode),
|
name = stringResource(R.string.customize_transparency_mode),
|
||||||
navController = navController
|
navController = navController,
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +314,8 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
snapPoints = listOf(75f),
|
snapPoints = listOf(75f),
|
||||||
startIcon = "\uDBC0\uDEA1",
|
startIcon = "\uDBC0\uDEA1",
|
||||||
endIcon = "\uDBC0\uDEA9",
|
endIcon = "\uDBC0\uDEA9",
|
||||||
independent = true
|
independent = true,
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
if (state.capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
|
if (state.capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
|
||||||
@@ -263,14 +324,31 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
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),
|
||||||
checked = volumeSwipeEnabled,
|
checked = volumeSwipeEnabled,
|
||||||
onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it) }
|
onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it) },
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Box (
|
||||||
|
modifier = Modifier.then(
|
||||||
|
if (!state.isPremium) {
|
||||||
|
Modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||||
|
event.changes.forEach { it.consume() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = stringResource(R.string.volume_swipe_speed),
|
label = stringResource(R.string.volume_swipe_speed),
|
||||||
description = stringResource(R.string.volume_swipe_speed_description),
|
description = stringResource(R.string.volume_swipe_speed_description),
|
||||||
options = volumeSwipeSpeedOptions.values.toList(),
|
options = volumeSwipeSpeedOptions.values.toList(),
|
||||||
selectedOption = selectedVolumeSwipeSpeed?: stringResource(R.string.default_option),
|
selectedOption = selectedVolumeSwipeSpeed
|
||||||
|
?: stringResource(R.string.default_option),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedVolumeSwipeSpeed = newValue
|
selectedVolumeSwipeSpeed = newValue
|
||||||
viewModel.setControlCommandByte(
|
viewModel.setControlCommandByte(
|
||||||
@@ -284,6 +362,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if (!hearingAidEnabled.value&& BuildConfig.FLAVOR == "xposed") {
|
// if (!hearingAidEnabled.value&& BuildConfig.FLAVOR == "xposed") {
|
||||||
// Text(
|
// Text(
|
||||||
@@ -507,7 +586,6 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,6 +612,9 @@ private fun DropdownMenuComponent(
|
|||||||
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
||||||
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
var parentDragActive by remember { mutableStateOf(false) }
|
var parentDragActive by remember { mutableStateOf(false) }
|
||||||
|
var previousIdx by remember { mutableStateOf<Int?>(null) }
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()){
|
Column(modifier = Modifier.fillMaxWidth()){
|
||||||
Column(
|
Column(
|
||||||
@@ -593,7 +674,11 @@ private fun DropdownMenuComponent(
|
|||||||
val touch = touchOffset ?: current
|
val touch = touchOffset ?: current
|
||||||
val posInPopupY = current.y - touch.y
|
val posInPopupY = current.y - touch.y
|
||||||
val idx = (posInPopupY / itemHeightPx).toInt()
|
val idx = (posInPopupY / itemHeightPx).toInt()
|
||||||
|
if (idx != previousIdx) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
|
||||||
|
}
|
||||||
parentHoveredIndex = idx
|
parentHoveredIndex = idx
|
||||||
|
previousIdx = idx
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
parentDragActive = false
|
parentDragActive = false
|
||||||
@@ -604,6 +689,9 @@ private fun DropdownMenuComponent(
|
|||||||
lastDismissTime = System.currentTimeMillis()
|
lastDismissTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (parentHoveredIndex != null && parentHoveredIndex in options.indices) {
|
||||||
|
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
|
||||||
|
}
|
||||||
parentHoveredIndex = null
|
parentHoveredIndex = null
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
+40
-7
@@ -16,14 +16,17 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
|
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.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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -33,21 +36,29 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
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.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 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.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
|
fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel, navController: NavController) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
@@ -62,6 +73,27 @@ fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
if (!state.isPremium) {
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("purchase_screen")
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.unlock_advanced_features),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = Color.White
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
val sliderValue = remember {
|
val sliderValue = remember {
|
||||||
mutableFloatStateOf(
|
mutableFloatStateOf(
|
||||||
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH]?.getOrNull(
|
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH]?.getOrNull(
|
||||||
@@ -90,7 +122,8 @@ fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
|
|||||||
startIcon = "",
|
startIcon = "",
|
||||||
endIcon = "",
|
endIcon = "",
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.adaptive_audio_description)
|
description = stringResource(R.string.adaptive_audio_description),
|
||||||
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+34
-46
@@ -18,13 +18,12 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
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.Arrangement
|
||||||
@@ -74,25 +73,25 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
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.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.composables.AudioSettings
|
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||||
import me.kavishdevar.librepods.composables.BatteryView
|
import me.kavishdevar.librepods.data.AirPodsPro3
|
||||||
import me.kavishdevar.librepods.composables.CallControlSettings
|
import me.kavishdevar.librepods.data.Capability
|
||||||
import me.kavishdevar.librepods.composables.ConnectionSettings
|
import me.kavishdevar.librepods.presentation.components.AboutCard
|
||||||
import me.kavishdevar.librepods.composables.HearingHealthSettings
|
import me.kavishdevar.librepods.presentation.components.AudioSettings
|
||||||
import me.kavishdevar.librepods.composables.MicrophoneSettings
|
import me.kavishdevar.librepods.presentation.components.BatteryView
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.presentation.components.CallControlSettings
|
||||||
import me.kavishdevar.librepods.composables.NoiseControlSettings
|
import me.kavishdevar.librepods.presentation.components.ConnectionSettings
|
||||||
import me.kavishdevar.librepods.composables.PressAndHoldSettings
|
import me.kavishdevar.librepods.presentation.components.HearingHealthSettings
|
||||||
import me.kavishdevar.librepods.composables.StyledButton
|
import me.kavishdevar.librepods.presentation.components.MicrophoneSettings
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.presentation.components.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||||
import me.kavishdevar.librepods.utils.AirPodsPro3
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.Capability
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@@ -162,17 +161,14 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.then(
|
.then(if (blockTouches) Modifier.pointerInput(Unit) {
|
||||||
if (blockTouches) Modifier.pointerInput(Unit) {
|
|
||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
while (true) {
|
while (true) {
|
||||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||||
event.changes.forEach { it.consume() }
|
event.changes.forEach { it.consume() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else Modifier
|
} else Modifier)) {
|
||||||
)
|
|
||||||
) {
|
|
||||||
item(key = "spacer_top") { Spacer(modifier = Modifier.height(topPadding)) }
|
item(key = "spacer_top") { Spacer(modifier = Modifier.height(topPadding)) }
|
||||||
item(key = "battery") {
|
item(key = "battery") {
|
||||||
BatteryView(
|
BatteryView(
|
||||||
@@ -199,7 +195,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
state.instance?.model?.capabilities?.contains(Capability.PPE) == true
|
state.instance?.model?.capabilities?.contains(Capability.PPE) == true
|
||||||
|
|
||||||
if (hasHearingAidCapability || hasPPECapability) {
|
if (hasHearingAidCapability || hasPPECapability) {
|
||||||
if (hasPPECapability || (BuildConfig.FLAVOR == "xposed" && hasHearingAidCapability)) item(
|
if (hasPPECapability || (state.vendorIdHook && hasHearingAidCapability)) item(
|
||||||
key = "spacer_hearing_health"
|
key = "spacer_hearing_health"
|
||||||
) { Spacer(modifier = Modifier.height(24.dp)) }
|
) { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
item(key = "hearing_health") {
|
item(key = "hearing_health") {
|
||||||
@@ -207,7 +203,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
hasPPECapability = hasPPECapability,
|
hasPPECapability = hasPPECapability,
|
||||||
hasHearingAidCapability = hasHearingAidCapability,
|
hasHearingAidCapability = hasHearingAidCapability,
|
||||||
isXposed = BuildConfig.FLAVOR == "xposed"
|
vendorIdHook = state.vendorIdHook
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,27 +268,26 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
item(key = "upgrade_button") {
|
item(key = "upgrade_button") {
|
||||||
val context = LocalContext.current
|
|
||||||
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
|
||||||
|
|
||||||
if (!state.isPremium) {
|
if (!state.isPremium) {
|
||||||
Spacer(modifier = Modifier.height(28.dp))
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
StyledButton(
|
StyledButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.purchase(context)
|
navController.navigate("purchase_screen")
|
||||||
},
|
},
|
||||||
backdrop = rememberLayerBackdrop(),
|
backdrop = rememberLayerBackdrop(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
maxScale = 0.05f,
|
maxScale = 0.05f,
|
||||||
tint = Color(0xFF916100)
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(
|
||||||
|
0xFFE59900
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.unlock_all_features),
|
stringResource(R.string.unlock_advanced_features),
|
||||||
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 = textColor
|
color = Color.White
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -320,11 +315,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG]?.getOrNull(
|
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG]?.getOrNull(
|
||||||
0
|
0
|
||||||
) == 0x01.toByte()
|
) == 0x01.toByte()
|
||||||
val loudSoundReduction =
|
|
||||||
viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)
|
|
||||||
?.getOrNull(0) == 0x01.toByte()
|
|
||||||
|
|
||||||
val isXposed = BuildConfig.FLAVOR == "xposed"
|
|
||||||
AudioSettings(
|
AudioSettings(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
adaptiveVolumeCapability = adaptiveVolumeCapability,
|
adaptiveVolumeCapability = adaptiveVolumeCapability,
|
||||||
@@ -345,14 +336,14 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
checked
|
checked
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
loudSoundReductionChecked = loudSoundReduction,
|
loudSoundReductionChecked = state.loudSoundReductionEnabled,
|
||||||
onLoudSoundReductionCheckedChange = {
|
onLoudSoundReductionCheckedChange = {
|
||||||
viewModel.setATTCharacteristicValue(
|
viewModel.setATTCharacteristicValue(
|
||||||
ATTHandles.LOUD_SOUND_REDUCTION,
|
ATTHandles.LOUD_SOUND_REDUCTION,
|
||||||
byteArrayOf(if (it) 0x01.toByte() else 0x00.toByte())
|
byteArrayOf(if (it) 0x01.toByte() else 0x00.toByte())
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isXposed = isXposed,
|
vendorIdHook = state.vendorIdHook,
|
||||||
isPremium = state.isPremium
|
isPremium = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -484,11 +475,8 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
|||||||
tapCount.intValue = 0
|
tapCount.intValue = 0
|
||||||
viewModel.activateDemoMode()
|
viewModel.activateDemoMode()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
}) {
|
||||||
}
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.airpods_not_connected), style = TextStyle(
|
text = stringResource(R.string.airpods_not_connected), style = TextStyle(
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
+299
-48
@@ -16,12 +16,16 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
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.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
|
||||||
@@ -42,9 +46,14 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
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
|
||||||
@@ -54,6 +63,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
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.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
@@ -61,26 +71,26 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.presentation.components.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.viewmodel.AppSettingsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||||
|
import java.util.Locale.getDefault
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(
|
fun AppSettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController, viewModel: AppSettingsViewModel = viewModel()
|
||||||
viewModel: AppSettingsViewModel = viewModel()
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
StyledScaffold(
|
StyledScaffold(
|
||||||
title = stringResource(R.string.app_settings)
|
title = stringResource(R.string.settings)
|
||||||
) { topPadding, hazeState, bottomPadding ->
|
) { topPadding, hazeState, bottomPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -96,23 +106,23 @@ fun AppSettingsScreen(
|
|||||||
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
|
||||||
|
|
||||||
if (!uiState.isPremium) {
|
if (!state.isPremium) {
|
||||||
StyledButton(
|
StyledButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.purchase(context)
|
navController.navigate("purchase_screen")
|
||||||
},
|
},
|
||||||
backdrop = rememberLayerBackdrop(),
|
backdrop = rememberLayerBackdrop(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
maxScale = 0.05f,
|
maxScale = 0.05f,
|
||||||
tint = Color(0xFF916100)
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.unlock_all_features),
|
stringResource(R.string.unlock_advanced_features),
|
||||||
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 = textColor
|
color = Color.White
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -122,9 +132,9 @@ fun AppSettingsScreen(
|
|||||||
title = stringResource(R.string.widget),
|
title = stringResource(R.string.widget),
|
||||||
label = stringResource(R.string.show_phone_battery_in_widget),
|
label = stringResource(R.string.show_phone_battery_in_widget),
|
||||||
description = stringResource(R.string.show_phone_battery_in_widget_description),
|
description = stringResource(R.string.show_phone_battery_in_widget_description),
|
||||||
checked = uiState.showPhoneBatteryInWidget,
|
checked = state.showPhoneBatteryInWidget,
|
||||||
onCheckedChange = viewModel::setShowPhoneBatteryInWidget,
|
onCheckedChange = viewModel::setShowPhoneBatteryInWidget,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -149,10 +159,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.conversational_awareness_pause_music),
|
label = stringResource(R.string.conversational_awareness_pause_music),
|
||||||
description = stringResource(R.string.conversational_awareness_pause_music_description),
|
description = stringResource(R.string.conversational_awareness_pause_music_description),
|
||||||
checked = uiState.conversationalAwarenessPauseMusicEnabled,
|
checked = state.conversationalAwarenessPauseMusicEnabled,
|
||||||
onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled,
|
onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -164,16 +174,16 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.relative_conversational_awareness_volume),
|
label = stringResource(R.string.relative_conversational_awareness_volume),
|
||||||
description = stringResource(R.string.relative_conversational_awareness_volume_description),
|
description = stringResource(R.string.relative_conversational_awareness_volume_description),
|
||||||
checked = uiState.relativeConversationalAwarenessVolumeEnabled,
|
checked = state.relativeConversationalAwarenessVolumeEnabled,
|
||||||
onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled,
|
onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
val conversationalAwarenessVolume = uiState.conversationalAwarenessVolume
|
val conversationalAwarenessVolume = state.conversationalAwarenessVolume
|
||||||
LaunchedEffect(conversationalAwarenessVolume) {
|
LaunchedEffect(conversationalAwarenessVolume) {
|
||||||
viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume)
|
viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume)
|
||||||
}
|
}
|
||||||
@@ -182,11 +192,12 @@ fun AppSettingsScreen(
|
|||||||
label = stringResource(R.string.conversational_awareness_volume),
|
label = stringResource(R.string.conversational_awareness_volume),
|
||||||
value = conversationalAwarenessVolume,
|
value = conversationalAwarenessVolume,
|
||||||
valueRange = 10f..85f,
|
valueRange = 10f..85f,
|
||||||
|
snapPoints = listOf(44f),
|
||||||
startLabel = "10%",
|
startLabel = "10%",
|
||||||
endLabel = "85%",
|
endLabel = "85%",
|
||||||
onValueChange = { newValue -> viewModel.setConversationalAwarenessVolume(newValue) },
|
onValueChange = { newValue -> viewModel.setConversationalAwarenessVolume(newValue) },
|
||||||
independent = true,
|
independent = true,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!BuildConfig.PLAY_BUILD) {
|
if (!BuildConfig.PLAY_BUILD) {
|
||||||
@@ -198,7 +209,7 @@ fun AppSettingsScreen(
|
|||||||
name = stringResource(R.string.set_custom_camera_package),
|
name = stringResource(R.string.set_custom_camera_package),
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.isPremium) viewModel.setShowCameraDialog(true)
|
if (state.isPremium) viewModel.setShowCameraDialog(true)
|
||||||
},
|
},
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.camera_control_app_description)
|
description = stringResource(R.string.camera_control_app_description)
|
||||||
@@ -211,9 +222,9 @@ fun AppSettingsScreen(
|
|||||||
title = stringResource(R.string.ear_detection),
|
title = stringResource(R.string.ear_detection),
|
||||||
label = stringResource(R.string.disconnect_when_not_wearing),
|
label = stringResource(R.string.disconnect_when_not_wearing),
|
||||||
description = stringResource(R.string.disconnect_when_not_wearing_description),
|
description = stringResource(R.string.disconnect_when_not_wearing_description),
|
||||||
checked = uiState.disconnectWhenNotWearing,
|
checked = state.disconnectWhenNotWearing,
|
||||||
onCheckedChange = viewModel::setDisconnectWhenNotWearing,
|
onCheckedChange = viewModel::setDisconnectWhenNotWearing,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,10 +250,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.takeover_disconnected),
|
label = stringResource(R.string.takeover_disconnected),
|
||||||
description = stringResource(R.string.takeover_disconnected_desc),
|
description = stringResource(R.string.takeover_disconnected_desc),
|
||||||
checked = uiState.takeoverWhenDisconnected,
|
checked = state.takeoverWhenDisconnected,
|
||||||
onCheckedChange = viewModel::setTakeoverWhenDisconnected,
|
onCheckedChange = viewModel::setTakeoverWhenDisconnected,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -253,10 +264,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.takeover_idle),
|
label = stringResource(R.string.takeover_idle),
|
||||||
description = stringResource(R.string.takeover_idle_desc),
|
description = stringResource(R.string.takeover_idle_desc),
|
||||||
checked = uiState.takeoverWhenIdle,
|
checked = state.takeoverWhenIdle,
|
||||||
onCheckedChange = viewModel::setTakeoverWhenIdle,
|
onCheckedChange = viewModel::setTakeoverWhenIdle,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -267,10 +278,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.takeover_music),
|
label = stringResource(R.string.takeover_music),
|
||||||
description = stringResource(R.string.takeover_music_desc),
|
description = stringResource(R.string.takeover_music_desc),
|
||||||
checked = uiState.takeoverWhenMusic,
|
checked = state.takeoverWhenMusic,
|
||||||
onCheckedChange = viewModel::setTakeoverWhenMusic,
|
onCheckedChange = viewModel::setTakeoverWhenMusic,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -281,10 +292,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.takeover_call),
|
label = stringResource(R.string.takeover_call),
|
||||||
description = stringResource(R.string.takeover_call_desc),
|
description = stringResource(R.string.takeover_call_desc),
|
||||||
checked = uiState.takeoverWhenCall,
|
checked = state.takeoverWhenCall,
|
||||||
onCheckedChange = viewModel::setTakeoverWhenCall,
|
onCheckedChange = viewModel::setTakeoverWhenCall,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,10 +321,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.takeover_ringing_call),
|
label = stringResource(R.string.takeover_ringing_call),
|
||||||
description = stringResource(R.string.takeover_ringing_call_desc),
|
description = stringResource(R.string.takeover_ringing_call_desc),
|
||||||
checked = uiState.takeoverWhenRingingCall,
|
checked = state.takeoverWhenRingingCall,
|
||||||
onCheckedChange = viewModel::setTakeoverWhenRingingCall,
|
onCheckedChange = viewModel::setTakeoverWhenRingingCall,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
@@ -324,10 +335,10 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.takeover_media_start),
|
label = stringResource(R.string.takeover_media_start),
|
||||||
description = stringResource(R.string.takeover_media_start_desc),
|
description = stringResource(R.string.takeover_media_start_desc),
|
||||||
checked = uiState.takeoverWhenMediaStart,
|
checked = state.takeoverWhenMediaStart,
|
||||||
onCheckedChange = viewModel::setTakeoverWhenMediaStart,
|
onCheckedChange = viewModel::setTakeoverWhenMediaStart,
|
||||||
independent = false,
|
independent = false,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,13 +356,31 @@ fun AppSettingsScreen(
|
|||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.use_alternate_head_tracking_packets),
|
label = stringResource(R.string.use_alternate_head_tracking_packets),
|
||||||
description = stringResource(R.string.use_alternate_head_tracking_packets_description),
|
description = stringResource(R.string.use_alternate_head_tracking_packets_description),
|
||||||
checked = uiState.useAlternateHeadTrackingPackets,
|
checked = state.useAlternateHeadTrackingPackets,
|
||||||
onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets,
|
onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets,
|
||||||
independent = true,
|
independent = true,
|
||||||
enabled = uiState.isPremium
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (BuildConfig.FLAVOR == "xposed") {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth)
|
||||||
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.act_as_an_apple_device),
|
||||||
|
description = stringResource(R.string.act_as_an_apple_device_description) + "\n" + stringResource(
|
||||||
|
R.string.requires_xposed
|
||||||
|
).replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
|
||||||
|
checked = state.vendorIdHook,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
|
Toast.makeText(context, restartBluetoothText, Toast.LENGTH_SHORT).show()
|
||||||
|
viewModel.setVendorIdHook(enabled)
|
||||||
|
},
|
||||||
|
independent = true,
|
||||||
|
enabled = state.isPremium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// NavigationButton(
|
// NavigationButton(
|
||||||
// to = "troubleshooting",
|
// to = "troubleshooting",
|
||||||
@@ -361,6 +390,229 @@ fun AppSettingsScreen(
|
|||||||
// description = stringResource(R.string.troubleshooting_description)
|
// description = stringResource(R.string.troubleshooting_description)
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.contact), style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
backgroundColor, RoundedCornerShape(28.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
) {
|
||||||
|
NavigationButton(
|
||||||
|
to = "",
|
||||||
|
name = stringResource(R.string.email),
|
||||||
|
navController = navController,
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO).apply {
|
||||||
|
data = "mailto:".toUri()
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz"))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ")
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
"\n\n\n----------" +
|
||||||
|
"\nPhone details:" +
|
||||||
|
"\nDEVICE: ${Build.DEVICE}" +
|
||||||
|
"\nMANUFACTURER: ${Build.MANUFACTURER} (${Build.BRAND})" +
|
||||||
|
"\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" +
|
||||||
|
"\nVERSION: ${Build.DISPLAY} (${Build.VERSION.SDK_INT_FULL})" +
|
||||||
|
"\n\nApp details:" +
|
||||||
|
"\nVERSION: ${BuildConfig.VERSION_NAME}" +
|
||||||
|
"\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" +
|
||||||
|
"\nFLAVOR: ${BuildConfig.FLAVOR}" +
|
||||||
|
"\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
NavigationButton(
|
||||||
|
to = "",
|
||||||
|
name = stringResource(R.string.discord),
|
||||||
|
navController = navController,
|
||||||
|
onClick = {
|
||||||
|
val intent =
|
||||||
|
Intent(Intent.ACTION_VIEW, "https://discord.gg/Ts4wupXcmc".toUri())
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
NavigationButton(
|
||||||
|
to = "",
|
||||||
|
name = stringResource(R.string.github_issues),
|
||||||
|
navController = navController,
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
"https://github.com/kavishdevar/librepods/issues".toUri()
|
||||||
|
)
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.about), style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val rowHeight = remember { mutableStateOf(0.dp) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||||
|
.padding(top = 2.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
rowHeight.value = with(density) { coordinates.size.height.toDp() }
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.version), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.VERSION_NAME, style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.version_code), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.VERSION_CODE.toString(), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.flavor), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.FLAVOR, style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.build_type), style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.BUILD_TYPE,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
|
||||||
|
alpha = 0.8f
|
||||||
|
),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
@@ -370,9 +622,9 @@ fun AppSettingsScreen(
|
|||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
|
||||||
if (uiState.showCameraDialog) {
|
if (state.showCameraDialog) {
|
||||||
AlertDialog(onDismissRequest = { viewModel.setShowCameraDialog(false) }, title = {
|
AlertDialog(onDismissRequest = { viewModel.setShowCameraDialog(false) }, title = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.set_custom_camera_package),
|
stringResource(R.string.set_custom_camera_package),
|
||||||
@@ -388,13 +640,13 @@ fun AppSettingsScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = uiState.cameraPackageValue,
|
value = state.cameraPackageValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
viewModel.setCameraPackageValue(it)
|
viewModel.setCameraPackageValue(it)
|
||||||
viewModel.setCameraPackageError(null)
|
viewModel.setCameraPackageError(null)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isError = uiState.cameraPackageError != null,
|
isError = state.cameraPackageError != null,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Ascii,
|
keyboardType = KeyboardType.Ascii,
|
||||||
capitalization = KeyboardCapitalization.None
|
capitalization = KeyboardCapitalization.None
|
||||||
@@ -406,9 +658,9 @@ fun AppSettingsScreen(
|
|||||||
unfocusedBorderColor = if (isDarkTheme) Color.Gray else Color.LightGray
|
unfocusedBorderColor = if (isDarkTheme) Color.Gray else Color.LightGray
|
||||||
),
|
),
|
||||||
supportingText = {
|
supportingText = {
|
||||||
if (uiState.cameraPackageError != null) {
|
if (state.cameraPackageError != null) {
|
||||||
Text(
|
Text(
|
||||||
uiState.cameraPackageError ?: "",
|
state.cameraPackageError ?: "",
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -439,7 +691,6 @@ fun AppSettingsScreen(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-6
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
@@ -41,12 +41,12 @@ import androidx.compose.ui.unit.dp
|
|||||||
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 me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.SelectItem
|
import me.kavishdevar.librepods.presentation.components.SelectItem
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSelectList
|
import me.kavishdevar.librepods.presentation.components.StyledSelectList
|
||||||
import me.kavishdevar.librepods.services.AppListenerService
|
import me.kavishdevar.librepods.services.AppListenerService
|
||||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
|
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.StemPressType
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CameraControlScreen(viewModel: AirPodsViewModel) {
|
fun CameraControlScreen(viewModel: AirPodsViewModel) {
|
||||||
+5
-5
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
@@ -82,10 +82,10 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
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.presentation.components.StyledIconButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
import me.kavishdevar.librepods.data.isHeadTrackingData
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
+54
-63
@@ -21,8 +21,12 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RadialGradient
|
||||||
|
import android.graphics.Shader
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
@@ -72,7 +76,6 @@ import androidx.compose.ui.graphics.asAndroidPath
|
|||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
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.drawText
|
import androidx.compose.ui.text.drawText
|
||||||
@@ -83,6 +86,7 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
|||||||
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
|
||||||
@@ -91,13 +95,13 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
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.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.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 me.kavishdevar.librepods.presentation.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,7 +111,7 @@ import kotlin.random.Random
|
|||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
|
fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
viewModel.startHeadTracking()
|
viewModel.startHeadTracking()
|
||||||
@@ -163,25 +167,23 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(topPadding))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
if (!state.isPremium) {
|
if (!state.isPremium) {
|
||||||
StyledButton(
|
StyledButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.purchase(context)
|
navController.navigate("purchase_screen")
|
||||||
},
|
},
|
||||||
backdrop = rememberLayerBackdrop(),
|
backdrop = rememberLayerBackdrop(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
maxScale = 0.05f,
|
maxScale = 0.05f,
|
||||||
tint = Color(0xFF916100)
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.unlock_all_features),
|
stringResource(R.string.unlock_advanced_features),
|
||||||
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 = textColor
|
color = Color.White
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -192,31 +194,20 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
|
|||||||
label = "Head Gestures",
|
label = "Head Gestures",
|
||||||
checked = state.headGesturesEnabled,
|
checked = state.headGesturesEnabled,
|
||||||
onCheckedChange = { viewModel.setHeadGesturesEnabled(it) },
|
onCheckedChange = { viewModel.setHeadGesturesEnabled(it) },
|
||||||
enabled = state.isPremium
|
enabled = state.isPremium,
|
||||||
)
|
description = stringResource(R.string.head_gestures_details)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.head_gestures_details),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
color = textColor.copy(0.6f)
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 4.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
"Head Orientation",
|
"Head Orientation",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Bold,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
color = textColor
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp, top = 8.dp)
|
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
|
||||||
)
|
)
|
||||||
HeadVisualization()
|
HeadVisualization()
|
||||||
|
|
||||||
@@ -224,12 +215,12 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
|
|||||||
Text(
|
Text(
|
||||||
"Velocity",
|
"Velocity",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Bold,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
color = textColor
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp, top = 8.dp)
|
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
|
||||||
)
|
)
|
||||||
AccelerationPlot()
|
AccelerationPlot()
|
||||||
|
|
||||||
@@ -481,9 +472,9 @@ private fun HeadVisualization() {
|
|||||||
spherePath.close()
|
spherePath.close()
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
val paint = android.graphics.Paint().apply {
|
val paint = Paint().apply {
|
||||||
style = android.graphics.Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
shader = android.graphics.RadialGradient(
|
shader = RadialGradient(
|
||||||
center.x + sinY * faceRadius * 0.3f,
|
center.x + sinY * faceRadius * 0.3f,
|
||||||
center.y - sinP * faceRadius * 0.3f,
|
center.y - sinP * faceRadius * 0.3f,
|
||||||
faceRadius * 1.4f,
|
faceRadius * 1.4f,
|
||||||
@@ -495,14 +486,14 @@ private fun HeadVisualization() {
|
|||||||
backgroundColor.copy(alpha = 0.7f).toArgb()
|
backgroundColor.copy(alpha = 0.7f).toArgb()
|
||||||
),
|
),
|
||||||
floatArrayOf(0.3f, 0.5f, 0.7f, 0.8f, 1f),
|
floatArrayOf(0.3f, 0.5f, 0.7f, 0.8f, 1f),
|
||||||
android.graphics.Shader.TileMode.CLAMP
|
Shader.TileMode.CLAMP
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
drawPath(spherePath.asAndroidPath(), paint)
|
drawPath(spherePath.asAndroidPath(), paint)
|
||||||
|
|
||||||
val highlightPaint = android.graphics.Paint().apply {
|
val highlightPaint = Paint().apply {
|
||||||
style = android.graphics.Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
shader = android.graphics.RadialGradient(
|
shader = RadialGradient(
|
||||||
center.x - faceRadius * 0.4f - sinY * faceRadius * 0.5f,
|
center.x - faceRadius * 0.4f - sinY * faceRadius * 0.5f,
|
||||||
center.y - faceRadius * 0.4f - sinP * faceRadius * 0.5f,
|
center.y - faceRadius * 0.4f - sinP * faceRadius * 0.5f,
|
||||||
faceRadius * 0.9f,
|
faceRadius * 0.9f,
|
||||||
@@ -512,15 +503,15 @@ private fun HeadVisualization() {
|
|||||||
android.graphics.Color.TRANSPARENT
|
android.graphics.Color.TRANSPARENT
|
||||||
),
|
),
|
||||||
floatArrayOf(0f, 0.3f, 1f),
|
floatArrayOf(0f, 0.3f, 1f),
|
||||||
android.graphics.Shader.TileMode.CLAMP
|
Shader.TileMode.CLAMP
|
||||||
)
|
)
|
||||||
alpha = if (darkTheme) 30 else 60
|
alpha = if (darkTheme) 30 else 60
|
||||||
}
|
}
|
||||||
drawPath(spherePath.asAndroidPath(), highlightPaint)
|
drawPath(spherePath.asAndroidPath(), highlightPaint)
|
||||||
|
|
||||||
val secondaryHighlightPaint = android.graphics.Paint().apply {
|
val secondaryHighlightPaint = Paint().apply {
|
||||||
style = android.graphics.Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
shader = android.graphics.RadialGradient(
|
shader = RadialGradient(
|
||||||
center.x + faceRadius * 0.3f + sinY * faceRadius * 0.3f,
|
center.x + faceRadius * 0.3f + sinY * faceRadius * 0.3f,
|
||||||
center.y + faceRadius * 0.3f - sinP * faceRadius * 0.3f,
|
center.y + faceRadius * 0.3f - sinP * faceRadius * 0.3f,
|
||||||
faceRadius * 0.7f,
|
faceRadius * 0.7f,
|
||||||
@@ -529,15 +520,15 @@ private fun HeadVisualization() {
|
|||||||
android.graphics.Color.TRANSPARENT
|
android.graphics.Color.TRANSPARENT
|
||||||
),
|
),
|
||||||
floatArrayOf(0f, 1f),
|
floatArrayOf(0f, 1f),
|
||||||
android.graphics.Shader.TileMode.CLAMP
|
Shader.TileMode.CLAMP
|
||||||
)
|
)
|
||||||
alpha = if (darkTheme) 15 else 30
|
alpha = if (darkTheme) 15 else 30
|
||||||
}
|
}
|
||||||
drawPath(spherePath.asAndroidPath(), secondaryHighlightPaint)
|
drawPath(spherePath.asAndroidPath(), secondaryHighlightPaint)
|
||||||
|
|
||||||
val shadowPaint = android.graphics.Paint().apply {
|
val shadowPaint = Paint().apply {
|
||||||
style = android.graphics.Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
shader = android.graphics.RadialGradient(
|
shader = RadialGradient(
|
||||||
center.x + sinY * faceRadius * 0.5f,
|
center.x + sinY * faceRadius * 0.5f,
|
||||||
center.y - sinP * faceRadius * 0.5f,
|
center.y - sinP * faceRadius * 0.5f,
|
||||||
faceRadius * 1.1f,
|
faceRadius * 1.1f,
|
||||||
@@ -546,7 +537,7 @@ private fun HeadVisualization() {
|
|||||||
android.graphics.Color.BLACK
|
android.graphics.Color.BLACK
|
||||||
),
|
),
|
||||||
floatArrayOf(0.7f, 1f),
|
floatArrayOf(0.7f, 1f),
|
||||||
android.graphics.Shader.TileMode.CLAMP
|
Shader.TileMode.CLAMP
|
||||||
)
|
)
|
||||||
alpha = if (darkTheme) 40 else 20
|
alpha = if (darkTheme) 40 else 20
|
||||||
}
|
}
|
||||||
@@ -606,13 +597,13 @@ private fun HeadVisualization() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
val paint = android.graphics.Paint().apply {
|
val paint = Paint().apply {
|
||||||
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
||||||
textSize = 12.sp.toPx()
|
textSize = 12.sp.toPx()
|
||||||
textAlign = android.graphics.Paint.Align.RIGHT
|
textAlign = Paint.Align.RIGHT
|
||||||
typeface = android.graphics.Typeface.create(
|
typeface = Typeface.create(
|
||||||
"SF Pro",
|
"SF Pro",
|
||||||
android.graphics.Typeface.NORMAL
|
Typeface.NORMAL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,10 +717,10 @@ private fun AccelerationPlot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
val paint = android.graphics.Paint().apply {
|
val paint = Paint().apply {
|
||||||
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
||||||
textSize = 12.sp.toPx()
|
textSize = 12.sp.toPx()
|
||||||
textAlign = android.graphics.Paint.Align.RIGHT
|
textAlign = Paint.Align.RIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
drawText("${maxAbs.toInt()}", 30.dp.toPx(), 20.dp.toPx(), paint)
|
drawText("${maxAbs.toInt()}", 30.dp.toPx(), 20.dp.toPx(), paint)
|
||||||
@@ -742,20 +733,20 @@ private fun AccelerationPlot() {
|
|||||||
|
|
||||||
drawCircle(Color(0xFF007AFF), 5.dp.toPx(), Offset(width - 150.dp.toPx(), legendY))
|
drawCircle(Color(0xFF007AFF), 5.dp.toPx(), Offset(width - 150.dp.toPx(), legendY))
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
val paint = android.graphics.Paint().apply {
|
val paint = Paint().apply {
|
||||||
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
||||||
textSize = 12.sp.toPx()
|
textSize = 12.sp.toPx()
|
||||||
textAlign = android.graphics.Paint.Align.LEFT
|
textAlign = Paint.Align.LEFT
|
||||||
}
|
}
|
||||||
drawText("Horizontal", width - 140.dp.toPx(), textOffsetY, paint)
|
drawText("Horizontal", width - 140.dp.toPx(), textOffsetY, paint)
|
||||||
}
|
}
|
||||||
|
|
||||||
drawCircle(Color(0xFFFF3B30), 5.dp.toPx(), Offset(width - 70.dp.toPx(), legendY))
|
drawCircle(Color(0xFFFF3B30), 5.dp.toPx(), Offset(width - 70.dp.toPx(), legendY))
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
val paint = android.graphics.Paint().apply {
|
val paint = Paint().apply {
|
||||||
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
|
||||||
textSize = 12.sp.toPx()
|
textSize = 12.sp.toPx()
|
||||||
textAlign = android.graphics.Paint.Align.LEFT
|
textAlign = Paint.Align.LEFT
|
||||||
}
|
}
|
||||||
drawText("Vertical", width - 60.dp.toPx(), textOffsetY, paint)
|
drawText("Vertical", width - 60.dp.toPx(), textOffsetY, paint)
|
||||||
}
|
}
|
||||||
+10
-10
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -50,16 +50,16 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.HearingAidSettings
|
import me.kavishdevar.librepods.data.HearingAidSettings
|
||||||
import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
|
import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.sendHearingAidSettings
|
import me.kavishdevar.librepods.data.sendHearingAidSettings
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
+17
-16
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -62,15 +62,14 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.data.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.data.sendTransparencySettings
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.presentation.components.NavigationButton
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
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"
|
||||||
@@ -174,7 +173,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
|||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "hearing_aid_adjustments",
|
to = "hearing_aid_adjustments",
|
||||||
name = stringResource(R.string.adjustments),
|
name = stringResource(R.string.adjustments),
|
||||||
navController,
|
navController = navController,
|
||||||
independent = false
|
independent = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -193,7 +192,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
|||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "update_hearing_test",
|
to = "update_hearing_test",
|
||||||
name = stringResource(R.string.update_hearing_test),
|
name = stringResource(R.string.update_hearing_test),
|
||||||
navController,
|
navController = navController,
|
||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -237,7 +236,6 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
|||||||
Spacer(modifier = Modifier.height(bottomPadding))
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
showDialog = showDialog,
|
showDialog = showDialog,
|
||||||
title = "Enable Hearing Aid",
|
title = "Enable Hearing Aid",
|
||||||
@@ -256,12 +254,11 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
|||||||
hearingAidEnabled.value = true
|
hearingAidEnabled.value = true
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val data = viewModel.getATTCharacteristicValue(ATTHandles.TRANSPARENCY) ?: byteArrayOf()
|
if (state.hearingAidData.isEmpty()) {
|
||||||
if (data.isEmpty()) {
|
|
||||||
Log.w(TAG, "read failed")
|
Log.w(TAG, "read failed")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val parsed = parseTransparencySettingsResponse(data)
|
val parsed = parseTransparencySettingsResponse(state.hearingAidData)
|
||||||
val disabledSettings = parsed.copy(enabled = false)
|
val disabledSettings = parsed.copy(enabled = false)
|
||||||
sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings)
|
sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -269,6 +266,10 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDismiss = {
|
||||||
|
hearingAidEnabled.value = false
|
||||||
|
showDialog.value = false
|
||||||
|
},
|
||||||
hazeState = hazeStateS.value,
|
hazeState = hazeStateS.value,
|
||||||
// backdrop = backdrop
|
// backdrop = backdrop
|
||||||
)
|
)
|
||||||
+13
-12
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -33,11 +33,11 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
|||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
|
fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
|
||||||
@@ -54,20 +54,19 @@ fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
if (BuildConfig.FLAVOR == "xposed") {
|
if (state.vendorIdHook) {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
title = stringResource(R.string.environmental_noise),
|
title = stringResource(R.string.environmental_noise),
|
||||||
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),
|
||||||
checked = viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)
|
checked = state.loudSoundReductionEnabled,
|
||||||
?.get(0)?.toInt() == 1,
|
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.setATTCharacteristicValue(
|
viewModel.setATTCharacteristicValue(
|
||||||
ATTHandles.LOUD_SOUND_REDUCTION,
|
ATTHandles.LOUD_SOUND_REDUCTION,
|
||||||
byteArrayOf(if (it) 1.toByte() else 0.toByte())
|
byteArrayOf(if (it) 1.toByte() else 0.toByte())
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
// attHandle = ATTHandles.LOUD_SOUND_REDUCTION
|
enabled = state.isPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@@ -83,7 +82,9 @@ fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
|
|||||||
viewModel.setControlCommandBoolean(
|
viewModel.setControlCommandBoolean(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, it
|
AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, it
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
enabled = state.isPremium
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-15
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -28,12 +28,9 @@ 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.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.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
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.navigation.NavController
|
||||||
@@ -42,17 +39,9 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.produceLibraries
|
import com.mikepenz.aboutlibraries.ui.compose.produceLibraries
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
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.launch
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
private var debounceJob: Job? = null
|
||||||
@@ -76,7 +65,7 @@ fun OpenSourceLicensesScreen(navController: NavController) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(spacerHeight))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = LocalContext.current
|
||||||
val libraries by produceLibraries {
|
val libraries by produceLibraries {
|
||||||
context.resources.openRawResource(R.raw.aboutlibraries)
|
context.resources.openRawResource(R.raw.aboutlibraries)
|
||||||
.bufferedReader()
|
.bufferedReader()
|
||||||
+15
-13
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalStdlibApi::class, ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalStdlibApi::class, ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -49,25 +49,26 @@ 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.presentation.components.SelectItem
|
||||||
import me.kavishdevar.librepods.composables.StyledButton
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSelectList
|
import me.kavishdevar.librepods.presentation.components.StyledSelectList
|
||||||
import me.kavishdevar.librepods.constants.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPress(viewModel: AirPodsViewModel, name: String) {
|
fun LongPress(viewModel: AirPodsViewModel, name: String, 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
|
||||||
|
|
||||||
@@ -124,20 +125,20 @@ fun LongPress(viewModel: AirPodsViewModel, name: String) {
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
StyledButton(
|
StyledButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.purchase(context)
|
navController.navigate("purchase_screen")
|
||||||
},
|
},
|
||||||
backdrop = rememberLayerBackdrop(),
|
backdrop = rememberLayerBackdrop(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
maxScale = 0.05f,
|
maxScale = 0.05f,
|
||||||
tint = Color(0xFF916100)
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.unlock_all_features),
|
stringResource(R.string.unlock_advanced_features),
|
||||||
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 = textColor
|
color = Color.White
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,6 +153,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String) {
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
+496
@@ -0,0 +1,496 @@
|
|||||||
|
/*
|
||||||
|
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.presentation.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import dev.chrisbanes.haze.hazeSource
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||||
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurchaseScreen(
|
||||||
|
viewModel: PurchaseViewModel = viewModel(),
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
|
||||||
|
StyledScaffold(
|
||||||
|
title = stringResource(R.string.unlock_advanced_features)
|
||||||
|
) { topPadding, hazeState, bottomPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.layerBackdrop(backdrop)
|
||||||
|
.hazeSource(state = hazeState)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)
|
||||||
|
val cardBackgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
LaunchedEffect(state.isPremium) {
|
||||||
|
if (state.isPremium) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!state.isPremium) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Free features",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(cardBackgroundColor, RoundedCornerShape(28.dp))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.ear_detection),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.ear_detection_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.battery),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.battery_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.noise_control),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.noise_control_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (BuildConfig.FLAVOR == "xposed") {
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.hearing_aid) + " (" + stringResource(
|
||||||
|
R.string.requires_xposed
|
||||||
|
) + ")",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.hearing_aid_description).split("\n\n")[0],
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Advanced features",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(cardBackgroundColor, RoundedCornerShape(28.dp))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.conversational_awareness),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.conversational_awareness_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.head_gestures),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.head_gestures_details),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.advanced_device_settings),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.advanced_device_settings_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.automatic_connection),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.automatic_connection_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.customizations),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.customizations_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = Color(0x40888888),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.support_the_development),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.support_development_description),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.feature_availability_disclaimer),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.purchase(context)
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.buy),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = Color.White
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
StyledButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.restorePurchases()
|
||||||
|
},
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxScale = 0.05f,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.restore_purchases),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = textColor
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -60,8 +60,8 @@ 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.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
|
||||||
+10
-42
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -42,7 +42,6 @@ import androidx.compose.material3.Slider
|
|||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
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.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -68,15 +67,13 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.data.TransparencySettings
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.data.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.TransparencySettings
|
import me.kavishdevar.librepods.data.sendTransparencySettings
|
||||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
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
|
||||||
|
|
||||||
@@ -91,8 +88,6 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
|||||||
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 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)
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
@@ -151,23 +146,6 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val transparencyListener = remember {
|
|
||||||
object : (ByteArray) -> Unit {
|
|
||||||
override fun invoke(value: ByteArray) {
|
|
||||||
val parsed = parseTransparencySettingsResponse(value)
|
|
||||||
enabled.value = parsed.enabled
|
|
||||||
amplificationSliderValue.floatValue = parsed.netAmplification
|
|
||||||
balanceSliderValue.floatValue = parsed.balance
|
|
||||||
toneSliderValue.floatValue = parsed.leftTone
|
|
||||||
ambientNoiseReductionSliderValue.floatValue =
|
|
||||||
parsed.leftAmbientNoiseReduction
|
|
||||||
conversationBoostEnabled.value = parsed.leftConversationBoost
|
|
||||||
eq.value = parsed.leftEQ.copyOf()
|
|
||||||
Log.d(TAG, "Updated transparency settings from notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
enabled.value,
|
enabled.value,
|
||||||
amplificationSliderValue.floatValue,
|
amplificationSliderValue.floatValue,
|
||||||
@@ -211,18 +189,9 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
|||||||
sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value)
|
sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
Log.d(TAG, "Connecting to ATT...")
|
Log.d(TAG, "Connecting to ATT...")
|
||||||
try {
|
try {
|
||||||
attManager.enableNotifications(ATTHandles.TRANSPARENCY)
|
|
||||||
attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
|
||||||
|
|
||||||
// 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 {
|
||||||
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
||||||
@@ -242,7 +211,7 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
|||||||
for (attempt in 1..3) {
|
for (attempt in 1..3) {
|
||||||
initialReadAttempts.intValue = attempt
|
initialReadAttempts.intValue = attempt
|
||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
val data = state.transparencyData
|
||||||
parsedSettings = parseTransparencySettingsResponse(data = data)
|
parsedSettings = parseTransparencySettingsResponse(data = data)
|
||||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -275,8 +244,7 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show transparency mode section if SDP offset is available
|
if (state.vendorIdHook) {
|
||||||
if (BuildConfig.FLAVOR == "xposed") {
|
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
label = stringResource(R.string.transparency_mode),
|
label = stringResource(R.string.transparency_mode),
|
||||||
checked = enabled.value,
|
checked = enabled.value,
|
||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -94,7 +94,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.LogCollector
|
import me.kavishdevar.librepods.utils.LogCollector
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
+6
-6
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -59,12 +59,12 @@ import dev.chrisbanes.haze.hazeSource
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.HearingAidSettings
|
import me.kavishdevar.librepods.data.HearingAidSettings
|
||||||
import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
|
import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.sendHearingAidSettings
|
import me.kavishdevar.librepods.data.sendHearingAidSettings
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
|
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
|
||||||
+5
-4
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.presentation.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -48,8 +48,8 @@ import androidx.compose.ui.unit.sp
|
|||||||
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 me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||||
import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VersionScreen(viewModel: AirPodsViewModel) {
|
fun VersionScreen(viewModel: AirPodsViewModel) {
|
||||||
@@ -80,7 +80,8 @@ fun VersionScreen(viewModel: AirPodsViewModel) {
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.ui.theme
|
package me.kavishdevar.librepods.presentation.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.ui.theme
|
package me.kavishdevar.librepods.presentation.theme
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.ui.theme
|
package me.kavishdevar.librepods.presentation.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
+112
-55
@@ -16,9 +16,8 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.viewmodel
|
package me.kavishdevar.librepods.presentation.viewmodel
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -28,25 +27,29 @@ import android.util.Log
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.billing.BillingManager
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.constants.Battery
|
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
|
||||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.data.AirPodsInstance
|
||||||
import me.kavishdevar.librepods.constants.StemAction
|
import me.kavishdevar.librepods.data.AirPodsModels
|
||||||
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
|
import me.kavishdevar.librepods.data.Battery
|
||||||
|
import me.kavishdevar.librepods.data.BatteryComponent
|
||||||
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
|
import me.kavishdevar.librepods.data.Capability
|
||||||
import me.kavishdevar.librepods.data.ControlCommandRepository
|
import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||||
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
|
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
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")
|
@Suppress("ArrayInDataClass")
|
||||||
data class AirPodsUiState(
|
data class AirPodsUiState(
|
||||||
@@ -81,7 +84,12 @@ data class AirPodsUiState(
|
|||||||
val leftAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
|
val leftAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||||
val rightAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
|
val rightAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||||
|
|
||||||
|
val loudSoundReductionEnabled: Boolean = false,
|
||||||
|
val transparencyData: ByteArray = byteArrayOf(),
|
||||||
|
val hearingAidData: ByteArray = byteArrayOf(),
|
||||||
|
|
||||||
val isPremium: Boolean = false,
|
val isPremium: Boolean = false,
|
||||||
|
val vendorIdHook: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
class AirPodsViewModel(
|
class AirPodsViewModel(
|
||||||
@@ -90,8 +98,14 @@ class AirPodsViewModel(
|
|||||||
private val controlRepo: ControlCommandRepository,
|
private val controlRepo: ControlCommandRepository,
|
||||||
private val appContext: Context
|
private val appContext: Context
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow(
|
||||||
private val _uiState = MutableStateFlow(AirPodsUiState(deviceName = sharedPreferences.getString("name", "AirPods Pro") ?: "AirPods Pro"))
|
AirPodsUiState(
|
||||||
|
deviceName = sharedPreferences.getString(
|
||||||
|
"name",
|
||||||
|
"AirPods Pro"
|
||||||
|
) ?: "AirPods Pro"
|
||||||
|
)
|
||||||
|
)
|
||||||
val uiState: StateFlow<AirPodsUiState> = _uiState
|
val uiState: StateFlow<AirPodsUiState> = _uiState
|
||||||
|
|
||||||
private var isDemoMode = false
|
private var isDemoMode = false
|
||||||
@@ -99,17 +113,16 @@ class AirPodsViewModel(
|
|||||||
|
|
||||||
private var billingFirstCollectDone = false
|
private var billingFirstCollectDone = false
|
||||||
|
|
||||||
private val listeners = mutableMapOf<
|
private val listeners =
|
||||||
ControlCommandIdentifiers,
|
mutableMapOf<ControlCommandIdentifiers, AACPManager.ControlCommandListener>()
|
||||||
AACPManager.ControlCommandListener
|
|
||||||
>()
|
private val xposedRemotePref = XposedRemotePrefProvider.create()
|
||||||
|
|
||||||
private lateinit var broadcastReceiver: BroadcastReceiver
|
private lateinit var broadcastReceiver: BroadcastReceiver
|
||||||
|
|
||||||
private val _cameraAction = MutableStateFlow(
|
private val _cameraAction = MutableStateFlow(
|
||||||
sharedPreferences.getString("camera_action", null)
|
sharedPreferences.getString("camera_action", null)
|
||||||
?.let { value -> AACPManager.Companion.StemPressType.entries.find { it.name == value } }
|
?.let { value -> AACPManager.Companion.StemPressType.entries.find { it.name == value } })
|
||||||
)
|
|
||||||
|
|
||||||
val cameraAction: StateFlow<AACPManager.Companion.StemPressType?> = _cameraAction
|
val cameraAction: StateFlow<AACPManager.Companion.StemPressType?> = _cameraAction
|
||||||
|
|
||||||
@@ -129,6 +142,7 @@ class AirPodsViewModel(
|
|||||||
setupControlObservers()
|
setupControlObservers()
|
||||||
observeBilling()
|
observeBilling()
|
||||||
loadControlList()
|
loadControlList()
|
||||||
|
observeATT()
|
||||||
if (isDemoMode) activateDemoMode()
|
if (isDemoMode) activateDemoMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +162,9 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeBilling() {
|
private fun observeBilling() {
|
||||||
if (!isDemoMode) viewModelScope.launch {
|
if (isDemoMode) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (!BuildConfig.PLAY_BUILD) billingFirstCollectDone = true // FOSS doesn't send multiple events
|
||||||
BillingManager.provider.isPremium.collect { premium ->
|
BillingManager.provider.isPremium.collect { premium ->
|
||||||
if (!billingFirstCollectDone) {
|
if (!billingFirstCollectDone) {
|
||||||
billingFirstCollectDone = true
|
billingFirstCollectDone = true
|
||||||
@@ -156,7 +172,10 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
if (!premium) {
|
if (!premium) {
|
||||||
Log.d("AirPodsViewModel", "we are not premium")
|
Log.d("AirPodsViewModel", "we are not premium")
|
||||||
setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, false)
|
setControlCommandBoolean(
|
||||||
|
ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
||||||
|
false
|
||||||
|
)
|
||||||
setHeadGesturesEnabled(false)
|
setHeadGesturesEnabled(false)
|
||||||
} else {
|
} else {
|
||||||
Log.d("AirPodsViewModel", "we are premium")
|
Log.d("AirPodsViewModel", "we are premium")
|
||||||
@@ -183,7 +202,8 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
AirPodsNotifications.BATTERY_DATA -> {
|
AirPodsNotifications.BATTERY_DATA -> {
|
||||||
val data = intent.getParcelableArrayListExtra("data", Battery::class.java)?.toList() ?: emptyList()
|
val data = intent.getParcelableArrayListExtra("data", Battery::class.java)
|
||||||
|
?.toList() ?: emptyList()
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(battery = data)
|
it.copy(battery = data)
|
||||||
}
|
}
|
||||||
@@ -213,15 +233,12 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
appContext.registerReceiver(
|
appContext.registerReceiver(
|
||||||
broadcastReceiver,
|
broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED
|
||||||
filter,
|
|
||||||
Context.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setControlCommandValue(
|
fun setControlCommandValue(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, value: ByteArray
|
||||||
value: ByteArray
|
|
||||||
) {
|
) {
|
||||||
if (!isDemoMode) controlRepo.setValue(identifier, value)
|
if (!isDemoMode) controlRepo.setValue(identifier, value)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@@ -232,25 +249,21 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setControlCommandBoolean(
|
fun setControlCommandBoolean(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, enabled: Boolean
|
||||||
enabled: Boolean
|
|
||||||
) {
|
) {
|
||||||
setControlCommandValue(
|
setControlCommandValue(
|
||||||
identifier,
|
identifier, if (enabled) byteArrayOf(0x01) else byteArrayOf(0x02)
|
||||||
if (enabled) byteArrayOf(0x01) else byteArrayOf(0x02)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setControlCommandInt(
|
fun setControlCommandInt(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, value: Int
|
||||||
value: Int
|
|
||||||
) {
|
) {
|
||||||
setControlCommandValue(identifier, byteArrayOf(value.toByte()))
|
setControlCommandValue(identifier, byteArrayOf(value.toByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setControlCommandByte(
|
fun setControlCommandByte(
|
||||||
identifier: ControlCommandIdentifiers,
|
identifier: ControlCommandIdentifiers, value: Byte
|
||||||
value: Byte
|
|
||||||
) {
|
) {
|
||||||
setControlCommandValue(identifier, byteArrayOf(value))
|
setControlCommandValue(identifier, byteArrayOf(value))
|
||||||
}
|
}
|
||||||
@@ -267,7 +280,7 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners[identifier] = listener
|
listeners[identifier] = listener as AACPManager.ControlCommandListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// I'm lazy, sorry.
|
// I'm lazy, sorry.
|
||||||
@@ -309,8 +322,7 @@ class AirPodsViewModel(
|
|||||||
service.let { service ->
|
service.let { service ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLocallyConnected = service.isConnected(),
|
isLocallyConnected = service.isConnected(), battery = service.getBattery()
|
||||||
battery = service.getBattery()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,11 +330,24 @@ class AirPodsViewModel(
|
|||||||
|
|
||||||
private fun loadSharedPreferences() {
|
private fun loadSharedPreferences() {
|
||||||
val offListeningModeEnabled = sharedPreferences.getBoolean("off_listening_mode", true)
|
val offListeningModeEnabled = sharedPreferences.getBoolean("off_listening_mode", true)
|
||||||
val automaticEarDetectionEnabled = sharedPreferences.getBoolean("automatic_ear_detection", true)
|
val automaticEarDetectionEnabled =
|
||||||
val automaticConnectionEnabled = sharedPreferences.getBoolean("automatic_connection_ctrl_cmd", true)
|
sharedPreferences.getBoolean("automatic_ear_detection", true)
|
||||||
|
val automaticConnectionEnabled =
|
||||||
|
sharedPreferences.getBoolean("automatic_connection_ctrl_cmd", true)
|
||||||
val headGesturesEnabled = sharedPreferences.getBoolean("head_gestures", 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 leftAction = StemAction.valueOf(
|
||||||
val rightAction = StemAction.valueOf(sharedPreferences.getString("right_long_press_action", "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES")
|
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"
|
||||||
|
)
|
||||||
|
val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
|
||||||
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -331,7 +356,8 @@ class AirPodsViewModel(
|
|||||||
automaticConnectionEnabled = automaticConnectionEnabled,
|
automaticConnectionEnabled = automaticConnectionEnabled,
|
||||||
headGesturesEnabled = headGesturesEnabled,
|
headGesturesEnabled = headGesturesEnabled,
|
||||||
leftAction = leftAction,
|
leftAction = leftAction,
|
||||||
rightAction = rightAction
|
rightAction = rightAction,
|
||||||
|
vendorIdHook = vendorIdHook
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,14 +391,12 @@ class AirPodsViewModel(
|
|||||||
name = "AirPods",
|
name = "AirPods",
|
||||||
model = AirPodsModels.getModelByModelNumber("A3049")!!,
|
model = AirPodsModels.getModelByModelNumber("A3049")!!,
|
||||||
actualModelNumber = "A3049",
|
actualModelNumber = "A3049",
|
||||||
aacpManager = service.aacpManager,
|
|
||||||
serialNumber = null,
|
serialNumber = null,
|
||||||
leftSerialNumber = null,
|
leftSerialNumber = null,
|
||||||
rightSerialNumber = null,
|
rightSerialNumber = null,
|
||||||
version1 = null,
|
version1 = null,
|
||||||
version2 = null,
|
version2 = null,
|
||||||
version3 = null,
|
version3 = null,
|
||||||
attManager = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@@ -381,7 +405,11 @@ class AirPodsViewModel(
|
|||||||
instance = instance,
|
instance = instance,
|
||||||
modelName = instance.model.displayName,
|
modelName = instance.model.displayName,
|
||||||
actualModel = instance.actualModelNumber,
|
actualModel = instance.actualModelNumber,
|
||||||
serialNumbers = listOf(instance.serialNumber ?: "", instance.leftSerialNumber ?: "", instance.rightSerialNumber ?: ""),
|
serialNumbers = listOf(
|
||||||
|
instance.serialNumber ?: "",
|
||||||
|
instance.leftSerialNumber ?: "",
|
||||||
|
instance.rightSerialNumber ?: ""
|
||||||
|
),
|
||||||
version1 = instance.version1 ?: "",
|
version1 = instance.version1 ?: "",
|
||||||
version2 = instance.version2 ?: "",
|
version2 = instance.version2 ?: "",
|
||||||
version3 = instance.version3 ?: ""
|
version3 = instance.version3 ?: ""
|
||||||
@@ -408,11 +436,42 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setATTCharacteristicValue(handle: ATTHandles, value: ByteArray) {
|
fun setATTCharacteristicValue(handle: ATTHandles, value: ByteArray) {
|
||||||
|
if (handle == ATTHandles.LOUD_SOUND_REDUCTION) {
|
||||||
|
_uiState.update { it.copy(loudSoundReductionEnabled = value[0].toInt() == 0x01) }
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
service.attManager?.write(handle, value)
|
service.attManager?.write(handle, value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getATTCharacteristicValue(handle: ATTHandles): ByteArray? {
|
fun refreshATT() {
|
||||||
return service.attManager?.read(handle)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val loudSoundReduction =
|
||||||
|
runCatching { service.attManager?.read(ATTHandles.LOUD_SOUND_REDUCTION) }.getOrNull()
|
||||||
|
val transparencyData =
|
||||||
|
runCatching { service.attManager?.read(ATTHandles.TRANSPARENCY) }.getOrNull()?: byteArrayOf()
|
||||||
|
val hearingAid =
|
||||||
|
runCatching { service.attManager?.read(ATTHandles.HEARING_AID) }.getOrNull()?: byteArrayOf()
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
loudSoundReductionEnabled = loudSoundReduction?.get(0)?.toInt() == 0x01,
|
||||||
|
transparencyData = transparencyData,
|
||||||
|
hearingAidData = hearingAid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeATT() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
service.attManager?.connect()
|
||||||
|
service.attManager?.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
|
||||||
|
service.attManager?.enableNotifications(ATTHandles.TRANSPARENCY)
|
||||||
|
service.attManager?.enableNotifications(ATTHandles.HEARING_AID)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
refreshATT()
|
||||||
|
delay(10000)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAutomaticEarDetectionEnabled(enabled: Boolean) {
|
fun setAutomaticEarDetectionEnabled(enabled: Boolean) {
|
||||||
@@ -435,9 +494,9 @@ class AirPodsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun purchase(context: Context) {
|
// fun purchase(context: Context) {
|
||||||
BillingManager.provider.purchase(context as Activity)
|
// BillingManager.provider.purchase(context as Activity)
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun activateDemoMode() {
|
fun activateDemoMode() {
|
||||||
isDemoMode = true
|
isDemoMode = true
|
||||||
@@ -448,14 +507,12 @@ class AirPodsViewModel(
|
|||||||
name = "AirPods Pro (Demo)",
|
name = "AirPods Pro (Demo)",
|
||||||
model = AirPodsModels.getModelByModelNumber("A3049")!!,
|
model = AirPodsModels.getModelByModelNumber("A3049")!!,
|
||||||
actualModelNumber = "A3049",
|
actualModelNumber = "A3049",
|
||||||
aacpManager = service.aacpManager,
|
|
||||||
serialNumber = "DEMO123",
|
serialNumber = "DEMO123",
|
||||||
leftSerialNumber = "L-DEMO",
|
leftSerialNumber = "L-DEMO",
|
||||||
rightSerialNumber = "R-DEMO",
|
rightSerialNumber = "R-DEMO",
|
||||||
version1 = "1.0",
|
version1 = "1.0",
|
||||||
version2 = "1.0",
|
version2 = "1.0",
|
||||||
version3 = "1.0",
|
version3 = "1.0",
|
||||||
attManager = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
+16
-5
@@ -1,6 +1,5 @@
|
|||||||
package me.kavishdevar.librepods.viewmodel
|
package me.kavishdevar.librepods.presentation.viewmodel
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
@@ -10,7 +9,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.billing.BillingManager
|
import me.kavishdevar.librepods.billing.BillingManager
|
||||||
|
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||||
|
import me.kavishdevar.librepods.utils.NativeBridge
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
data class AppSettingsUiState(
|
data class AppSettingsUiState(
|
||||||
@@ -29,6 +31,7 @@ data class AppSettingsUiState(
|
|||||||
val showCameraDialog: Boolean = false,
|
val showCameraDialog: Boolean = false,
|
||||||
val cameraPackageValue: String = "",
|
val cameraPackageValue: String = "",
|
||||||
val cameraPackageError: String? = null,
|
val cameraPackageError: String? = null,
|
||||||
|
val vendorIdHook: Boolean = false,
|
||||||
val isPremium: Boolean = false
|
val isPremium: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,6 +41,8 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
private val _uiState = MutableStateFlow(AppSettingsUiState())
|
private val _uiState = MutableStateFlow(AppSettingsUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val xposedRemotePref = XposedRemotePrefProvider.create()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadSettings()
|
loadSettings()
|
||||||
observeBilling()
|
observeBilling()
|
||||||
@@ -66,9 +71,13 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
takeoverWhenMediaStart = sharedPreferences.getBoolean("takeover_when_media_start", false),
|
takeoverWhenMediaStart = sharedPreferences.getBoolean("takeover_when_media_start", false),
|
||||||
useAlternateHeadTrackingPackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", true),
|
useAlternateHeadTrackingPackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", true),
|
||||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
|
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
|
||||||
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: ""
|
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "",
|
||||||
|
vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (BuildConfig.FLAVOR == "xposed") {
|
||||||
|
NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowPhoneBatteryInWidget(enabled: Boolean) {
|
fun setShowPhoneBatteryInWidget(enabled: Boolean) {
|
||||||
@@ -152,7 +161,9 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
setShowCameraDialog(false)
|
setShowCameraDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun purchase(context: Context) {
|
fun setVendorIdHook(enabled: Boolean) {
|
||||||
BillingManager.provider.purchase(context as Activity)
|
NativeBridge.setSdpHook(enabled)
|
||||||
|
xposedRemotePref.putBoolean("vendor_id_hook", enabled)
|
||||||
|
_uiState.update { it.copy(vendorIdHook = enabled) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package me.kavishdevar.librepods.presentation.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
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
|
||||||
|
|
||||||
|
data class PurchaseUiState(
|
||||||
|
val isPremium: Boolean = false,
|
||||||
|
val price: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class PurchaseViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
private val _uiState = MutableStateFlow(PurchaseUiState())
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeBilling()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeBilling() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
BillingManager.provider.isPremium.collect { premium ->
|
||||||
|
_uiState.update { it.copy(isPremium = premium) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
BillingManager.provider.price.collect { price ->
|
||||||
|
_uiState.update { it.copy(price = price) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun purchase(context: Context) {
|
||||||
|
BillingManager.provider.purchase(context as Activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restorePurchases() {
|
||||||
|
BillingManager.provider.queryPurchases()
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.widgets
|
package me.kavishdevar.librepods.presentation.widgets
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
+2
-2
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.widgets
|
package me.kavishdevar.librepods.presentation.widgets
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
@@ -29,7 +29,7 @@ import android.util.Log
|
|||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
class NoiseControlWidget : AppWidgetProvider() {
|
class NoiseControlWidget : AppWidgetProvider() {
|
||||||
@@ -35,9 +35,9 @@ import android.util.Log
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
import me.kavishdevar.librepods.data.NoiseControlMode
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
|||||||
@@ -83,25 +83,28 @@ import kotlinx.coroutines.withTimeout
|
|||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.MainActivity
|
import me.kavishdevar.librepods.MainActivity
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||||
import me.kavishdevar.librepods.constants.Battery
|
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.StemPressType
|
||||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
import me.kavishdevar.librepods.bluetooth.ATTManager
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.bluetooth.BLEManager
|
||||||
import me.kavishdevar.librepods.constants.StemAction
|
import me.kavishdevar.librepods.bluetooth.BluetoothConnectionManager
|
||||||
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
import me.kavishdevar.librepods.data.AirPodsInstance
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.data.AirPodsModels
|
||||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
|
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
import me.kavishdevar.librepods.data.Battery
|
||||||
import me.kavishdevar.librepods.utils.AirPodsInstance
|
import me.kavishdevar.librepods.data.BatteryComponent
|
||||||
import me.kavishdevar.librepods.utils.AirPodsModels
|
import me.kavishdevar.librepods.data.BatteryStatus
|
||||||
import me.kavishdevar.librepods.utils.BLEManager
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
import me.kavishdevar.librepods.utils.BluetoothConnectionManager
|
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||||
|
import me.kavishdevar.librepods.data.isHeadTrackingData
|
||||||
|
import me.kavishdevar.librepods.presentation.overlays.IslandType
|
||||||
|
import me.kavishdevar.librepods.presentation.overlays.IslandWindow
|
||||||
|
import me.kavishdevar.librepods.presentation.overlays.PopupWindow
|
||||||
|
import me.kavishdevar.librepods.presentation.widgets.BatteryWidget
|
||||||
|
import me.kavishdevar.librepods.presentation.widgets.NoiseControlWidget
|
||||||
import me.kavishdevar.librepods.utils.GestureDetector
|
import me.kavishdevar.librepods.utils.GestureDetector
|
||||||
import me.kavishdevar.librepods.utils.HeadTracking
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
import me.kavishdevar.librepods.utils.IslandType
|
|
||||||
import me.kavishdevar.librepods.utils.IslandWindow
|
|
||||||
import me.kavishdevar.librepods.utils.MediaController
|
import me.kavishdevar.librepods.utils.MediaController
|
||||||
import me.kavishdevar.librepods.utils.PopupWindow
|
|
||||||
import me.kavishdevar.librepods.utils.SystemApisUtils
|
import me.kavishdevar.librepods.utils.SystemApisUtils
|
||||||
import me.kavishdevar.librepods.utils.SystemApisUtils.DEVICE_TYPE_UNTETHERED_HEADSET
|
import me.kavishdevar.librepods.utils.SystemApisUtils.DEVICE_TYPE_UNTETHERED_HEADSET
|
||||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_COMPANION_APP
|
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_COMPANION_APP
|
||||||
@@ -121,8 +124,6 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_
|
|||||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_CHARGING
|
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_CHARGING
|
||||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ICON
|
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ICON
|
||||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
|
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
|
||||||
import me.kavishdevar.librepods.widgets.BatteryWidget
|
|
||||||
import me.kavishdevar.librepods.widgets.NoiseControlWidget
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
@@ -1060,8 +1061,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
version1 = config.airpodsVersion1,
|
version1 = config.airpodsVersion1,
|
||||||
version2 = config.airpodsVersion2,
|
version2 = config.airpodsVersion2,
|
||||||
version3 = config.airpodsVersion3,
|
version3 = config.airpodsVersion3,
|
||||||
aacpManager = aacpManager,
|
|
||||||
attManager = attManager
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sendBroadcast(
|
sendBroadcast(
|
||||||
@@ -1765,8 +1764,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("KotlinUnreachableCode")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
private fun showSocketConnectionFailureNotification(errorMessage: String) {
|
private fun showSocketConnectionFailureNotification(errorMessage: String) {
|
||||||
|
return // something causes too many notifications. turning off for now
|
||||||
if (BuildConfig.FLAVOR != "xposed") {
|
if (BuildConfig.FLAVOR != "xposed") {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG,
|
||||||
@@ -1788,7 +1789,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
.setSmallIcon(R.drawable.airpods).setContentTitle("AirPods Connection Issue")
|
.setSmallIcon(R.drawable.airpods).setContentTitle("AirPods Connection Issue")
|
||||||
.setContentText("Unable to connect to AirPods over L2CAP").setStyle(
|
.setContentText("Unable to connect to AirPods over L2CAP").setStyle(
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
"Your AirPods are connected via Bluetooth, but LibrePods couldn't connect to AirPods using L2CAP. " + "Error: $errorMessage"
|
"Your AirPods are connected via Bluetooth, but LibrePods couldn't connect to AirPods using L2CAP. Error: $errorMessage"
|
||||||
)
|
)
|
||||||
).setContentIntent(pendingIntent).setCategory(Notification.CATEGORY_ERROR)
|
).setContentIntent(pendingIntent).setCategory(Notification.CATEGORY_ERROR)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH).setAutoCancel(true).build()
|
.setPriority(NotificationCompat.PRIORITY_HIGH).setAutoCancel(true).build()
|
||||||
@@ -2178,7 +2179,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
fun processHeadTrackingData(data: ByteArray) {
|
fun processHeadTrackingData(data: ByteArray) {
|
||||||
val horizontal = ByteBuffer.wrap(data, 51, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
|
val horizontal = ByteBuffer.wrap(data, 51, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
|
||||||
val vertical = ByteBuffer.wrap(data, 53, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
|
val vertical = ByteBuffer.wrap(data, 53, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
|
||||||
|
try {
|
||||||
gestureDetector?.processHeadOrientation(horizontal, vertical)
|
gestureDetector?.processHeadOrientation(horizontal, vertical)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "gesture detector on ${data.toHexString()}: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var connectionReceiver: BroadcastReceiver
|
private lateinit var connectionReceiver: BroadcastReceiver
|
||||||
@@ -2666,8 +2671,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
this@AirPodsService.device = device
|
this@AirPodsService.device = device
|
||||||
|
|
||||||
BluetoothConnectionManager.setCurrentConnection(socket, device)
|
BluetoothConnectionManager.setCurrentConnection(socket, device)
|
||||||
|
val xposedRemotePref = XposedRemotePrefProvider.create()
|
||||||
if (BuildConfig.FLAVOR == "xposed") {
|
if (xposedRemotePref.getBoolean("vendor_id_hook", false)) {
|
||||||
attManager = ATTManager(adapter, device)
|
attManager = ATTManager(adapter, device)
|
||||||
attManager!!.connect()
|
attManager!!.connect()
|
||||||
}
|
}
|
||||||
@@ -2687,8 +2692,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
version1 = config.airpodsVersion1,
|
version1 = config.airpodsVersion1,
|
||||||
version2 = config.airpodsVersion2,
|
version2 = config.airpodsVersion2,
|
||||||
version3 = config.airpodsVersion3,
|
version3 = config.airpodsVersion3,
|
||||||
aacpManager = aacpManager,
|
|
||||||
attManager = attManager
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ val cameraPackages = mutableSetOf(
|
|||||||
var cameraOpen = false
|
var cameraOpen = false
|
||||||
private var currentCustomPackage: String? = null
|
private var currentCustomPackage: String? = null
|
||||||
|
|
||||||
class AppListenerService : AccessibilityService() {
|
class AppListenerService: AccessibilityService() {
|
||||||
private lateinit var prefs: android.content.SharedPreferences
|
private lateinit var prefs: android.content.SharedPreferences
|
||||||
private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||||
if (key == "custom_camera_package") {
|
if (key == "custom_camera_package") {
|
||||||
val newPackage = sharedPreferences.getString(key, null)
|
val newPackage = sharedPreferences.getString(key, null)
|
||||||
currentCustomPackage?.let { cameraPackages.remove(it) }
|
currentCustomPackage?.let { cameraPackages.remove(it) }
|
||||||
if (newPackage != null && newPackage.isNotBlank()) {
|
if (!newPackage.isNullOrBlank()) {
|
||||||
cameraPackages.add(newPackage)
|
cameraPackages.add(newPackage)
|
||||||
}
|
}
|
||||||
currentCustomPackage = newPackage
|
currentCustomPackage = newPackage
|
||||||
@@ -57,7 +57,7 @@ class AppListenerService : AccessibilityService() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
val customPackage = prefs.getString("custom_camera_package", null)
|
val customPackage = prefs.getString("custom_camera_package", null)
|
||||||
if (customPackage != null && customPackage.isNotBlank()) {
|
if (!customPackage.isNullOrBlank()) {
|
||||||
cameraPackages.add(customPackage)
|
cameraPackages.add(customPackage)
|
||||||
currentCustomPackage = customPackage
|
currentCustomPackage = customPackage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
|
|
||||||
fun isSupported(): Boolean {
|
fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
||||||
if (BuildConfig.PLAY_BUILD) {
|
|
||||||
val isPixel = Build.MANUFACTURER.lowercase() == "google"
|
val isPixel = Build.MANUFACTURER.lowercase() == "google"
|
||||||
val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
|
val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
|
||||||
|
|
||||||
@@ -39,11 +39,6 @@ fun isSupported(): Boolean {
|
|||||||
} else if (isOppoOrOnePlus) {
|
} else if (isOppoOrOnePlus) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
return if (BuildConfig.FLAVOR == "xposed") true
|
||||||
return true
|
else sharedPreferences.getBoolean("bypass_device_check", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*fun isSupported(): Boolean {
|
|
||||||
return true
|
|
||||||
}*/
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ object SystemApisUtils {
|
|||||||
)
|
)
|
||||||
method.invoke(device, key, value) as Boolean
|
method.invoke(device, key, value) as Boolean
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("SystemApisUtils", "Failed to set metadata for key $key", e)
|
Log.w("SystemApisUtils", "Failed to set metadata for key $key: ${e.message}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import io.github.libxposed.service.XposedService
|
||||||
|
|
||||||
|
object XposedServiceHolder {
|
||||||
|
var service: XposedService? = null
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods no conectados</string>
|
<string name="airpods_not_connected">AirPods no conectados</string>
|
||||||
<string name="airpods_not_connected_description">Por favor, conecta tus AirPods para acceder a los ajustes.</string>
|
<string name="airpods_not_connected_description">Por favor, conecta tus AirPods para acceder a los ajustes.</string>
|
||||||
<string name="back">Atrás</string>
|
<string name="back">Atrás</string>
|
||||||
<string name="app_settings">Personalización</string>
|
<string name="customizations">Personalización</string>
|
||||||
<string name="relative_conversational_awareness_volume">Volumen relativo</string>
|
<string name="relative_conversational_awareness_volume">Volumen relativo</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Reduce a un porcentaje del volumen actual en vez del volumen máximo.</string>
|
<string name="relative_conversational_awareness_volume_description">Reduce a un porcentaje del volumen actual en vez del volumen máximo.</string>
|
||||||
<string name="conversational_awareness_pause_music">Pausar música</string>
|
<string name="conversational_awareness_pause_music">Pausar música</string>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<string name="enter_enc_key_hex">Introducir 16-byte ENC_KEY como formato hexadecimal (32 caracteres):</string>
|
<string name="enter_enc_key_hex">Introducir 16-byte ENC_KEY como formato hexadecimal (32 caracteres):</string>
|
||||||
<string name="must_be_32_hex_chars">Debe tener exactamente 32 caracteres hexadecimales</string>
|
<string name="must_be_32_hex_chars">Debe tener exactamente 32 caracteres hexadecimales</string>
|
||||||
<string name="error_converting_hex">Error convirtiendo hex:</string>
|
<string name="error_converting_hex">Error convirtiendo hex:</string>
|
||||||
<string name="found_offset_restart_bluetooth">Offset encontrado. Por favor, reinicie el proceso Bluetooth</string>
|
<string name="found_offset_restart_bluetooth">Por favor, reinicie el proceso Bluetooth</string>
|
||||||
<string name="digital_assistant">Asistente Digital</string>
|
<string name="digital_assistant">Asistente Digital</string>
|
||||||
<string name="on">Activado</string>
|
<string name="on">Activado</string>
|
||||||
<string name="camera_remote">Control Remoto de Cámara</string>
|
<string name="camera_remote">Control Remoto de Cámara</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods non connectés</string>
|
<string name="airpods_not_connected">AirPods non connectés</string>
|
||||||
<string name="airpods_not_connected_description">Veuillez connecter vos AirPods pour accéder aux réglages.</string>
|
<string name="airpods_not_connected_description">Veuillez connecter vos AirPods pour accéder aux réglages.</string>
|
||||||
<string name="back">Retour</string>
|
<string name="back">Retour</string>
|
||||||
<string name="app_settings">Personnalisations</string>
|
<string name="customizations">Personnalisations</string>
|
||||||
<string name="relative_conversational_awareness_volume">Volume relatif</string>
|
<string name="relative_conversational_awareness_volume">Volume relatif</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Réduit à un pourcentage du volume actuel plutôt qu\'au volume maximum.</string>
|
<string name="relative_conversational_awareness_volume_description">Réduit à un pourcentage du volume actuel plutôt qu\'au volume maximum.</string>
|
||||||
<string name="conversational_awareness_pause_music">Mettre la musique en pause</string>
|
<string name="conversational_awareness_pause_music">Mettre la musique en pause</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods não conectados</string>
|
<string name="airpods_not_connected">AirPods não conectados</string>
|
||||||
<string name="airpods_not_connected_description">Por favor, conecte seus AirPods para acessar as configurações.</string>
|
<string name="airpods_not_connected_description">Por favor, conecte seus AirPods para acessar as configurações.</string>
|
||||||
<string name="back">Voltar</string>
|
<string name="back">Voltar</string>
|
||||||
<string name="app_settings">Personalizações</string>
|
<string name="customizations">Personalizações</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">Reduz para uma porcentagem do volume atual em vez do volume máximo.</string>
|
<string name="relative_conversational_awareness_volume_description">Reduz para uma porcentagem do volume atual em vez do volume máximo.</string>
|
||||||
<string name="conversational_awareness_pause_music">Pausar Música</string>
|
<string name="conversational_awareness_pause_music">Pausar Música</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods bağlı değil</string>
|
<string name="airpods_not_connected">AirPods bağlı değil</string>
|
||||||
<string name="airpods_not_connected_description">Ayarlara erişmek için lütfen AirPods\'unuzu bağlayın.</string>
|
<string name="airpods_not_connected_description">Ayarlara erişmek için lütfen AirPods\'unuzu bağlayın.</string>
|
||||||
<string name="back">Geri</string>
|
<string name="back">Geri</string>
|
||||||
<string name="app_settings">Özelleştirmeler</string>
|
<string name="customizations">Özelleştirmeler</string>
|
||||||
<string name="relative_conversational_awareness_volume">Göreceli ses</string>
|
<string name="relative_conversational_awareness_volume">Göreceli ses</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Maksimum ses yerine mevcut sesin yüzdesine göre azaltır.</string>
|
<string name="relative_conversational_awareness_volume_description">Maksimum ses yerine mevcut sesin yüzdesine göre azaltır.</string>
|
||||||
<string name="conversational_awareness_pause_music">Müziği Duraklat</string>
|
<string name="conversational_awareness_pause_music">Müziği Duraklat</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods не підключені</string>
|
<string name="airpods_not_connected">AirPods не підключені</string>
|
||||||
<string name="airpods_not_connected_description">Будь ласка, підключіть ваші AirPods, щоб отримати доступ до налаштувань.</string>
|
<string name="airpods_not_connected_description">Будь ласка, підключіть ваші AirPods, щоб отримати доступ до налаштувань.</string>
|
||||||
<string name="back">Назад</string>
|
<string name="back">Назад</string>
|
||||||
<string name="app_settings">Персоналізація</string>
|
<string name="customizations">Персоналізація</string>
|
||||||
<string name="relative_conversational_awareness_volume">Відносна гучність</string>
|
<string name="relative_conversational_awareness_volume">Відносна гучність</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Зменшує до відсотка від поточної гучності, а не від максимальної.</string>
|
<string name="relative_conversational_awareness_volume_description">Зменшує до відсотка від поточної гучності, а не від максимальної.</string>
|
||||||
<string name="conversational_awareness_pause_music">Призупинити Музику</string>
|
<string name="conversational_awareness_pause_music">Призупинити Музику</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods chưa được kết nối</string>
|
<string name="airpods_not_connected">AirPods chưa được kết nối</string>
|
||||||
<string name="airpods_not_connected_description">Vui lòng kết nối đến AirPods của bạn để truy cập cài đặt.</string>
|
<string name="airpods_not_connected_description">Vui lòng kết nối đến AirPods của bạn để truy cập cài đặt.</string>
|
||||||
<string name="back">Quay lại</string>
|
<string name="back">Quay lại</string>
|
||||||
<string name="app_settings">Tùy chỉnh</string>
|
<string name="customizations">Tùy chỉnh</string>
|
||||||
<string name="relative_conversational_awareness_volume">Âm lượng tương đối</string>
|
<string name="relative_conversational_awareness_volume">Âm lượng tương đối</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Giảm xuống phần trăm của âm lượng hiện tại thay vì âm lượng tối đa.</string>
|
<string name="relative_conversational_awareness_volume_description">Giảm xuống phần trăm của âm lượng hiện tại thay vì âm lượng tối đa.</string>
|
||||||
<string name="conversational_awareness_pause_music">Tạm dừng nhạc</string>
|
<string name="conversational_awareness_pause_music">Tạm dừng nhạc</string>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<string name="enter_enc_key_hex">Nhập ENC_KEY 16 byte dưới dạng chuỗi hex (32 ký tự):</string>
|
<string name="enter_enc_key_hex">Nhập ENC_KEY 16 byte dưới dạng chuỗi hex (32 ký tự):</string>
|
||||||
<string name="must_be_32_hex_chars">Phải chính xác 32 ký tự hex</string>
|
<string name="must_be_32_hex_chars">Phải chính xác 32 ký tự hex</string>
|
||||||
<string name="error_converting_hex">Lỗi chuyển đổi hex:</string>
|
<string name="error_converting_hex">Lỗi chuyển đổi hex:</string>
|
||||||
<string name="found_offset_restart_bluetooth">Đã tìm thấy độ lệch, vui lòng khởi động lại tiến trình Bluetooth</string>
|
<string name="found_offset_restart_bluetooth">vui lòng khởi động lại tiến trình Bluetooth</string>
|
||||||
<string name="digital_assistant">Trợ lý kỹ thuật số</string>
|
<string name="digital_assistant">Trợ lý kỹ thuật số</string>
|
||||||
<string name="on">Bật</string>
|
<string name="on">Bật</string>
|
||||||
<string name="camera_remote">Điều khiển máy ảnh từ xa</string>
|
<string name="camera_remote">Điều khiển máy ảnh từ xa</string>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods 未连接</string>
|
<string name="airpods_not_connected">AirPods 未连接</string>
|
||||||
<string name="airpods_not_connected_description">请连接 AirPods 以访问设置。</string>
|
<string name="airpods_not_connected_description">请连接 AirPods 以访问设置。</string>
|
||||||
<string name="back">返回</string>
|
<string name="back">返回</string>
|
||||||
<string name="app_settings">自定义</string>
|
<string name="customizations">自定义</string>
|
||||||
<string name="relative_conversational_awareness_volume">相对音量</string>
|
<string name="relative_conversational_awareness_volume">相对音量</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">降低到当前音量的百分比,而不是最大音量。</string>
|
<string name="relative_conversational_awareness_volume_description">降低到当前音量的百分比,而不是最大音量。</string>
|
||||||
<string name="conversational_awareness_pause_music">暂停音乐</string>
|
<string name="conversational_awareness_pause_music">暂停音乐</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">未連接 AirPods</string>
|
<string name="airpods_not_connected">未連接 AirPods</string>
|
||||||
<string name="airpods_not_connected_description">請連接你的 AirPods 以存取設定。</string>
|
<string name="airpods_not_connected_description">請連接你的 AirPods 以存取設定。</string>
|
||||||
<string name="back">返回</string>
|
<string name="back">返回</string>
|
||||||
<string name="app_settings">自訂</string>
|
<string name="customizations">自訂</string>
|
||||||
<string name="relative_conversational_awareness_volume">相對音量</string>
|
<string name="relative_conversational_awareness_volume">相對音量</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">降低至當前音量的百分比,而不是最大音量。</string>
|
<string name="relative_conversational_awareness_volume_description">降低至當前音量的百分比,而不是最大音量。</string>
|
||||||
<string name="conversational_awareness_pause_music">暫停音樂</string>
|
<string name="conversational_awareness_pause_music">暫停音樂</string>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods not connected</string>
|
<string name="airpods_not_connected">AirPods not connected</string>
|
||||||
<string name="airpods_not_connected_description">Please connect your AirPods to access settings.</string>
|
<string name="airpods_not_connected_description">Please connect your AirPods to access settings.</string>
|
||||||
<string name="back">Back</string>
|
<string name="back">Back</string>
|
||||||
<string name="app_settings">Customizations</string>
|
<string name="customizations">Customizations</string>
|
||||||
<string name="relative_conversational_awareness_volume">Relative volume</string>
|
<string name="relative_conversational_awareness_volume">Relative volume</string>
|
||||||
<string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string>
|
<string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string>
|
||||||
<string name="conversational_awareness_pause_music">Pause Music</string>
|
<string name="conversational_awareness_pause_music">Pause Music</string>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<string name="enter_enc_key_hex">Enter 16-byte ENC_KEY as hex string (32 characters):</string>
|
<string name="enter_enc_key_hex">Enter 16-byte ENC_KEY as hex string (32 characters):</string>
|
||||||
<string name="must_be_32_hex_chars">Must be exactly 32 hex characters</string>
|
<string name="must_be_32_hex_chars">Must be exactly 32 hex characters</string>
|
||||||
<string name="error_converting_hex">Error converting hex:</string>
|
<string name="error_converting_hex">Error converting hex:</string>
|
||||||
<string name="found_offset_restart_bluetooth">Found offset please restart the Bluetooth process</string>
|
<string name="found_offset_restart_bluetooth">Please restart the Bluetooth process</string>
|
||||||
<string name="digital_assistant">Digital Assistant</string>
|
<string name="digital_assistant">Digital Assistant</string>
|
||||||
<string name="on">On</string>
|
<string name="on">On</string>
|
||||||
<string name="camera_remote">Camera Remote</string>
|
<string name="camera_remote">Camera Remote</string>
|
||||||
@@ -210,5 +210,30 @@
|
|||||||
<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>
|
<string name="unlock_advanced_features">Unlock advanced features</string>
|
||||||
|
<string name="buy">Buy</string>
|
||||||
|
<string name="restore_purchases">Restore purchases</string>
|
||||||
|
<string name="ear_detection_description">Automatically stop playing audio when you take them off, and resume playback when you put them back on.</string>
|
||||||
|
<string name="battery">Battery</string>
|
||||||
|
<string name="battery_description">View accurate battery status in the app and notification.</string>
|
||||||
|
<string name="noise_control_description">Switch between listening modes directly from the app or Quick Settings.</string>
|
||||||
|
<string name="advanced_device_settings">Advanced device settings</string>
|
||||||
|
<string name="advanced_device_settings_description">Customize settings like Personalized Volume, Adaptive Audio, Pause media when falling asleep, and other Accessibility settings.</string>
|
||||||
|
<string name="automatic_connection">Automatic Connection</string>
|
||||||
|
<string name="automatic_connection_description">Enable and customize automatic connection to AirPods.</string>
|
||||||
|
<string name="customizations_description">Get access to app customizations, including phone battery in widget, conversational awareness volume, and many more upcoming customization features.</string>
|
||||||
|
<string name="support_the_development">Support the development</string>
|
||||||
|
<string name="support_development_description">LibrePods is developed by a single developer. Upgrading helps keep the app alive.</string>
|
||||||
|
<string name="feature_availability_disclaimer">Feature availability depends on your AirPods model and firmware version.</string>
|
||||||
|
<string name="contact">Contact</string>
|
||||||
|
<string name="email">E-Mail</string>
|
||||||
|
<string name="discord">Discord</string>
|
||||||
|
<string name="github_issues">GitHub Issues</string>
|
||||||
|
<string name="version_code">Version code</string>
|
||||||
|
<string name="flavor" translatable="false">Flavor</string>
|
||||||
|
<string name="build_type">Build type</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="requires_xposed">requires xposed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
|
||||||
|
class LibrePodsApplication: Application()
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
|
class XposedRemotePrefImpl: XposedRemotePref {
|
||||||
|
override fun isAvailable(): Boolean { return false }
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, def: Boolean): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String, value: Boolean) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import io.github.libxposed.api.XposedModule
|
||||||
|
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
|
||||||
|
import io.github.libxposed.api.XposedModuleInterface.PackageLoadedParam
|
||||||
|
|
||||||
|
private const val TAG = "LibrePodsHook"
|
||||||
|
|
||||||
|
@SuppressLint("DiscouragedApi", "PrivateApi")
|
||||||
|
class KotlinModule: XposedModule() {
|
||||||
|
override fun onModuleLoaded(param: ModuleLoadedParam) {
|
||||||
|
log(Log.INFO, TAG, "module initialized at :: ${param.processName}")
|
||||||
|
log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPackageLoaded(param: PackageLoadedParam) {
|
||||||
|
log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
|
||||||
|
|
||||||
|
if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
|
||||||
|
log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
||||||
|
try {
|
||||||
|
if (param.isFirstPackage) {
|
||||||
|
log(Log.INFO, TAG, "Loading native library for Bluetooth hook")
|
||||||
|
|
||||||
|
NativeBridge.setSdpHook(getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false))
|
||||||
|
System.loadLibrary("l2c_fcr_hook")
|
||||||
|
log(Log.INFO, TAG, "Native library loaded successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(Log.ERROR, TAG, "Failed to load native library: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.packageName == "com.google.android.settings") {
|
||||||
|
hookSettingsController(param, "com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.packageName == "com.android.settings") {
|
||||||
|
hookSettingsController(param, "com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hookSettingsController(param: PackageLoadedParam, className: String) {
|
||||||
|
log(Log.INFO, TAG, "Settings app detected, hooking Bluetooth icon handling")
|
||||||
|
try {
|
||||||
|
val headerControllerClass = Class.forName(className, false, param.defaultClassLoader)
|
||||||
|
val updateIconMethod = headerControllerClass.getDeclaredMethod(
|
||||||
|
"updateIcon",
|
||||||
|
ImageView::class.java,
|
||||||
|
String::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
hook(updateIconMethod).intercept { chain ->
|
||||||
|
try {
|
||||||
|
log(Log.INFO, TAG, "Bluetooth icon hook called with args: ${chain.args.joinToString(", ")}")
|
||||||
|
val imageView = chain.args[0] as? ImageView
|
||||||
|
val iconUri = chain.args[1] as? String
|
||||||
|
|
||||||
|
if (imageView == null || iconUri == null) {
|
||||||
|
return@intercept chain.proceed()
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = iconUri.toUri()
|
||||||
|
if (!uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
|
||||||
|
return@intercept chain.proceed()
|
||||||
|
}
|
||||||
|
|
||||||
|
log(Log.INFO, TAG, "Handling AirPods icon URI: $uri")
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
try {
|
||||||
|
val context = imageView.context
|
||||||
|
val packageName = uri.authority ?: return@post
|
||||||
|
val packageContext = context.createPackageContext(
|
||||||
|
packageName,
|
||||||
|
Context.CONTEXT_IGNORE_SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
|
val resPath = uri.pathSegments
|
||||||
|
if (resPath.size >= 2 && resPath[0] == "drawable") {
|
||||||
|
val resourceName = resPath[1]
|
||||||
|
val resourceId = packageContext.resources.getIdentifier(
|
||||||
|
resourceName, "drawable", packageName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (resourceId != 0) {
|
||||||
|
val drawable = packageContext.resources.getDrawable(
|
||||||
|
resourceId, packageContext.theme
|
||||||
|
)
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
imageView.alpha = 1.0f
|
||||||
|
log(Log.INFO, TAG, "Successfully loaded icon from resource: $resourceName")
|
||||||
|
} else {
|
||||||
|
log(Log.ERROR, TAG, "Resource not found: $resourceName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(Log.ERROR, TAG, "Error loading resource from URI $uri: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(Log.ERROR, TAG, "Error in Bluetooth icon hook: ${e.message}")
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(Log.INFO, TAG, "Successfully hooked updateIcon method in Bluetooth settings")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(Log.ERROR, TAG, "Failed to hook Bluetooth icon handler: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object NativeBridge {
|
||||||
|
external fun setSdpHook(enabled: Boolean)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.github.libxposed.service.XposedService
|
||||||
|
import io.github.libxposed.service.XposedServiceHelper
|
||||||
|
|
||||||
|
object XposedServiceHolder {
|
||||||
|
var service: XposedService? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object XposedInitializer: XposedServiceHelper.OnServiceListener {
|
||||||
|
private var initialized = false
|
||||||
|
|
||||||
|
fun ensureInit(context: Context) {
|
||||||
|
if (initialized) return
|
||||||
|
initialized = true
|
||||||
|
XposedServiceHelper.registerListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceBind(service: XposedService) {
|
||||||
|
XposedServiceHolder.service = service
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDied(service: XposedService) {
|
||||||
|
XposedServiceHolder.service = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <elf.h>
|
#include <elf.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
#include "l2c_fcr_hook.h"
|
#include "l2c_fcr_hook.h"
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ extern "C" {
|
|||||||
#include "xz.h"
|
#include "xz.h"
|
||||||
}
|
}
|
||||||
|
|
||||||
#define LOG_TAG "LibrePods"
|
#define LOG_TAG "LibrePodsHook"
|
||||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
@@ -41,8 +43,10 @@ static uint8_t (*original_l2c_fcr_chk_chan_modes)(void*) = nullptr;
|
|||||||
static tBTA_STATUS (*original_BTA_DmSetLocalDiRecord)(
|
static tBTA_STATUS (*original_BTA_DmSetLocalDiRecord)(
|
||||||
tSDP_DI_RECORD*, uint32_t*) = nullptr;
|
tSDP_DI_RECORD*, uint32_t*) = nullptr;
|
||||||
|
|
||||||
|
static std::atomic<bool> enableSdpHook(false);
|
||||||
|
|
||||||
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
||||||
LOGI("l2c_fcr_chk_chan_modes hooked");
|
LOGI("l2c_fcr_chk_chan_modes called");
|
||||||
uint8_t orig = 0;
|
uint8_t orig = 0;
|
||||||
if (original_l2c_fcr_chk_chan_modes)
|
if (original_l2c_fcr_chk_chan_modes)
|
||||||
orig = original_l2c_fcr_chk_chan_modes(p_ccb);
|
orig = original_l2c_fcr_chk_chan_modes(p_ccb);
|
||||||
@@ -55,7 +59,11 @@ tBTA_STATUS fake_BTA_DmSetLocalDiRecord(
|
|||||||
tSDP_DI_RECORD* p_device_info,
|
tSDP_DI_RECORD* p_device_info,
|
||||||
uint32_t* p_handle) {
|
uint32_t* p_handle) {
|
||||||
|
|
||||||
LOGI("BTA_DmSetLocalDiRecord hooked");
|
LOGI("BTA_DmSetLocalDiRecord called");
|
||||||
|
|
||||||
|
if (original_BTA_DmSetLocalDiRecord && enableSdpHook.load(std::memory_order_relaxed)) original_BTA_DmSetLocalDiRecord(p_device_info, p_handle);
|
||||||
|
|
||||||
|
LOGI("BTA_DmSetLocalDiRecord changing vendor id and source");
|
||||||
|
|
||||||
if (p_device_info) {
|
if (p_device_info) {
|
||||||
p_device_info->vendor = 0x004C;
|
p_device_info->vendor = 0x004C;
|
||||||
@@ -265,9 +273,9 @@ static bool hookLibrary(const char* libname) {
|
|||||||
findSymbolOffset(decompressed,
|
findSymbolOffset(decompressed,
|
||||||
"l2c_fcr_chk_chan_modes");
|
"l2c_fcr_chk_chan_modes");
|
||||||
|
|
||||||
// uint64_t sdp_offset =
|
uint64_t sdp_offset =
|
||||||
// findSymbolOffset(decompressed,
|
findSymbolOffset(decompressed,
|
||||||
// "BTA_DmSetLocalDiRecord");
|
"BTA_DmSetLocalDiRecord");
|
||||||
|
|
||||||
if (chk_offset) {
|
if (chk_offset) {
|
||||||
void* target =
|
void* target =
|
||||||
@@ -280,16 +288,16 @@ static bool hookLibrary(const char* libname) {
|
|||||||
LOGI("Hooked l2c_fcr_chk_chan_modes");
|
LOGI("Hooked l2c_fcr_chk_chan_modes");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (sdp_offset) {
|
if (sdp_offset) {
|
||||||
// void* target =
|
void* target =
|
||||||
// reinterpret_cast<void*>(base + sdp_offset);
|
reinterpret_cast<void*>(base + sdp_offset);
|
||||||
//
|
|
||||||
// hook_func(target,
|
hook_func(target,
|
||||||
// (void*)fake_BTA_DmSetLocalDiRecord,
|
(void*)fake_BTA_DmSetLocalDiRecord,
|
||||||
// (void**)&original_BTA_DmSetLocalDiRecord);
|
(void**)&original_BTA_DmSetLocalDiRecord);
|
||||||
//
|
|
||||||
// LOGI("Hooked BTA_DmSetLocalDiRecord");
|
LOGI("Hooked BTA_DmSetLocalDiRecord");
|
||||||
// }
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -315,10 +323,16 @@ extern "C"
|
|||||||
[[gnu::visibility("default")]]
|
[[gnu::visibility("default")]]
|
||||||
[[gnu::used]]
|
[[gnu::used]]
|
||||||
NativeOnModuleLoaded native_init(const NativeAPIEntries* entries) {
|
NativeOnModuleLoaded native_init(const NativeAPIEntries* entries) {
|
||||||
|
|
||||||
LOGI("LibrePods initialized");
|
|
||||||
|
|
||||||
hook_func = (HookFunType)entries->hook_func;
|
hook_func = (HookFunType)entries->hook_func;
|
||||||
|
LOGI("LibrePodsNativeHook initialized, sdp hook enabled: %d", enableSdpHook.load(std::memory_order_relaxed));
|
||||||
return on_library_loaded;
|
return on_library_loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_me_kavishdevar_librepods_utils_NativeBridge_setSdpHook(
|
||||||
|
JNIEnv*, jobject thiz, jboolean enable) {
|
||||||
|
enableSdpHook.store(enable, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
LOGI("sdp hook enabled: %d", enable);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import io.github.libxposed.service.XposedService
|
||||||
|
import io.github.libxposed.service.XposedServiceHelper
|
||||||
|
import me.kavishdevar.librepods.utils.XposedServiceHolder
|
||||||
|
|
||||||
|
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
XposedServiceHelper.registerListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceBind(p0: XposedService) {
|
||||||
|
XposedServiceHolder.service = p0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDied(p0: XposedService) {
|
||||||
|
XposedServiceHolder.service = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package me.kavishdevar.librepods.data
|
||||||
|
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import me.kavishdevar.librepods.utils.XposedServiceHolder
|
||||||
|
|
||||||
|
class XposedRemotePrefImpl: XposedRemotePref {
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return XposedServiceHolder.service != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, def: Boolean): Boolean {
|
||||||
|
val s = XposedServiceHolder.service ?: return def
|
||||||
|
return s.getRemotePreferences("me.kavishdevar.librepods").getBoolean(key, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
|
val s = XposedServiceHolder.service ?: return
|
||||||
|
s.getRemotePreferences("me.kavishdevar.librepods")
|
||||||
|
.edit { putBoolean(key, value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,104 +2,83 @@ package me.kavishdevar.librepods.utils
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import io.github.libxposed.api.XposedInterface
|
|
||||||
import io.github.libxposed.api.XposedInterface.AfterHookCallback
|
|
||||||
import io.github.libxposed.api.XposedModule
|
import io.github.libxposed.api.XposedModule
|
||||||
import io.github.libxposed.api.XposedModuleInterface
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
|
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
|
||||||
import io.github.libxposed.api.annotations.AfterInvocation
|
import io.github.libxposed.api.XposedModuleInterface.PackageLoadedParam
|
||||||
import io.github.libxposed.api.annotations.XposedHooker
|
|
||||||
import kotlin.jvm.java
|
private const val TAG = "LibrePodsHook"
|
||||||
|
|
||||||
private const val TAG = "AirPodsHook"
|
|
||||||
private lateinit var module: KotlinModule
|
|
||||||
@SuppressLint("DiscouragedApi", "PrivateApi")
|
@SuppressLint("DiscouragedApi", "PrivateApi")
|
||||||
class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) {
|
class KotlinModule: XposedModule() {
|
||||||
init {
|
override fun onModuleLoaded(param: ModuleLoadedParam) {
|
||||||
Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}")
|
log(Log.INFO, TAG, "module initialized at :: ${param.processName}")
|
||||||
module = this
|
log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageLoaded(param: XposedModuleInterface.PackageLoadedParam) {
|
override fun onPackageLoaded(param: PackageLoadedParam) {
|
||||||
super.onPackageLoaded(param)
|
log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
|
||||||
Log.i(TAG, "onPackageLoaded :: ${param.packageName}")
|
|
||||||
|
|
||||||
if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
|
if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
|
||||||
Log.i(TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (param.isFirstPackage) {
|
if (param.isFirstPackage) {
|
||||||
Log.i(TAG, "Loading native library for Bluetooth hook")
|
log(Log.INFO, TAG, "Loading native library for Bluetooth hook")
|
||||||
System.loadLibrary("l2c_fcr_hook")
|
System.loadLibrary("l2c_fcr_hook")
|
||||||
Log.i(TAG, "Native library loaded successfully")
|
val remotePrefValue = getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false)
|
||||||
|
log(Log.INFO, TAG, "sdp hook enabled (remote pref): $remotePrefValue")
|
||||||
|
NativeBridge.setSdpHook(remotePrefValue)
|
||||||
|
log(Log.INFO, TAG, "Native library loaded successfully")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load native library: ${e.message}", e)
|
log(Log.ERROR, TAG, "Failed to load native library: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param.packageName == "com.google.android.settings") {
|
if (param.packageName == "com.google.android.settings") {
|
||||||
Log.i(TAG, "Settings app detected, hooking Bluetooth icon handling")
|
hookSettingsController(param, "com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
||||||
try {
|
|
||||||
val headerControllerClass = param.classLoader.loadClass(
|
|
||||||
"com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
|
||||||
|
|
||||||
val updateIconMethod = headerControllerClass.getDeclaredMethod(
|
|
||||||
"updateIcon",
|
|
||||||
ImageView::class.java,
|
|
||||||
String::class.java)
|
|
||||||
|
|
||||||
hook(updateIconMethod, BluetoothIconHooker::class.java)
|
|
||||||
Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param.packageName == "com.android.settings") {
|
if (param.packageName == "com.android.settings") {
|
||||||
Log.i(TAG, "Settings app detected, hooking Bluetooth icon handling")
|
hookSettingsController(param, "com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
||||||
try {
|
}
|
||||||
val headerControllerClass = param.classLoader.loadClass(
|
}
|
||||||
"com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
|
||||||
|
|
||||||
|
private fun hookSettingsController(param: PackageLoadedParam, className: String) {
|
||||||
|
log(Log.INFO, TAG, "Settings app detected, hooking Bluetooth icon handling")
|
||||||
|
try {
|
||||||
|
val headerControllerClass = Class.forName(className, false, param.defaultClassLoader)
|
||||||
val updateIconMethod = headerControllerClass.getDeclaredMethod(
|
val updateIconMethod = headerControllerClass.getDeclaredMethod(
|
||||||
"updateIcon",
|
"updateIcon",
|
||||||
ImageView::class.java,
|
ImageView::class.java,
|
||||||
String::class.java)
|
String::class.java
|
||||||
|
)
|
||||||
|
|
||||||
hook(updateIconMethod, BluetoothIconHooker::class.java)
|
hook(updateIconMethod).intercept { chain ->
|
||||||
Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@XposedHooker
|
|
||||||
class BluetoothIconHooker : XposedInterface.Hooker {
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
@AfterInvocation
|
|
||||||
fun afterUpdateIcon(callback: AfterHookCallback) {
|
|
||||||
Log.i(TAG, "BluetoothIconHooker called with args: ${callback.args.joinToString(", ")}")
|
|
||||||
try {
|
try {
|
||||||
val imageView = callback.args[0] as ImageView
|
log(Log.INFO, TAG, "Bluetooth icon hook called with args: ${chain.args.joinToString(", ")}")
|
||||||
val iconUri = callback.args[1] as String
|
val imageView = chain.args[0] as? ImageView
|
||||||
|
val iconUri = chain.args[1] as? String
|
||||||
|
|
||||||
|
if (imageView == null || iconUri == null) {
|
||||||
|
return@intercept chain.proceed()
|
||||||
|
}
|
||||||
|
|
||||||
val uri = iconUri.toUri()
|
val uri = iconUri.toUri()
|
||||||
if (uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
|
if (!uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
|
||||||
Log.i(TAG, "Handling AirPods icon URI: $uri")
|
return@intercept chain.proceed()
|
||||||
|
}
|
||||||
|
|
||||||
|
log(Log.INFO, TAG, "Handling AirPods icon URI: $uri")
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
try {
|
try {
|
||||||
val context = imageView.context
|
val context = imageView.context
|
||||||
|
val packageName = uri.authority ?: return@post
|
||||||
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
||||||
try {
|
|
||||||
val packageName = uri.authority
|
|
||||||
val packageContext = context.createPackageContext(
|
val packageContext = context.createPackageContext(
|
||||||
packageName,
|
packageName,
|
||||||
Context.CONTEXT_IGNORE_SECURITY
|
Context.CONTEXT_IGNORE_SECURITY
|
||||||
@@ -116,34 +95,32 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
|
|||||||
val drawable = packageContext.resources.getDrawable(
|
val drawable = packageContext.resources.getDrawable(
|
||||||
resourceId, packageContext.theme
|
resourceId, packageContext.theme
|
||||||
)
|
)
|
||||||
|
|
||||||
imageView.setImageDrawable(drawable)
|
imageView.setImageDrawable(drawable)
|
||||||
imageView.alpha = 1.0f
|
imageView.alpha = 1.0f
|
||||||
|
log(Log.INFO, TAG, "Successfully loaded icon from resource: $resourceName")
|
||||||
callback.result = null
|
|
||||||
|
|
||||||
Log.i(TAG, "Successfully loaded icon from resource: $resourceName")
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Resource not found: $resourceName")
|
log(Log.ERROR, TAG, "Resource not found: $resourceName")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error loading resource from URI $uri: ${e.message}")
|
log(Log.ERROR, TAG, "Error loading resource from URI $uri: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error accessing context: ${e.message}")
|
log(Log.ERROR, TAG, "Error in Bluetooth icon hook: ${e.message}")
|
||||||
}
|
chain.proceed()
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error in BluetoothIconHooker: ${e.message}")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApplicationInfo(): ApplicationInfo {
|
log(Log.INFO, TAG, "Successfully hooked updateIcon method in Bluetooth settings")
|
||||||
return super.applicationInfo
|
} catch (e: Exception) {
|
||||||
|
log(Log.ERROR, TAG, "Failed to hook Bluetooth icon handler: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object NativeBridge {
|
||||||
|
external fun setSdpHook(enabled: Boolean)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
minApiVersion=100
|
minApiVersion=101
|
||||||
targetApiVersion=100
|
targetApiVersion=101
|
||||||
staticScope=true
|
staticScope=true
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ materialIconsCore = "1.7.8"
|
|||||||
backdrop = "2.0.0-alpha03"
|
backdrop = "2.0.0-alpha03"
|
||||||
billing = "8.3.0"
|
billing = "8.3.0"
|
||||||
hilt = "2.59.2"
|
hilt = "2.59.2"
|
||||||
|
xposed = "101.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
@@ -44,6 +45,8 @@ backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backd
|
|||||||
billing = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
|
billing = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
|
||||||
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
||||||
|
libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed" }
|
||||||
|
libxposed-service = { group = "io.github.libxposed", name = "service", version.ref = "xposed" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user