Compare commits

...

14 Commits

Author SHA1 Message Date
Kavish Devar
70f420dedb android: fix text color in email bottom sheet 2026-04-27 10:17:33 +05:30
Kavish Devar
23193ceb39 android: load native hook from split apks when base fails 2026-04-27 10:08:05 +05:30
Kavish Devar
cb246d1287 android: update dialog and add app info to incompatible page 2026-04-26 17:36:17 +05:30
Kavish Devar
95cd677da9 android: fix bypass on pixels on older A16 version; also check Xposed scope for compatibility 2026-04-26 16:23:29 +05:30
Kavish Devar
0d049d93fb ci: use latest release tag for changelog 2026-04-26 16:17:02 +05:30
Kavish Devar
469d948061 android: add xposed check and email form
too many emails with absolutely no content
2026-04-26 05:05:20 +05:30
Kavish Devar
f5d92768e2 android: rename util->utils in normal flavor 2026-04-26 01:07:12 +05:30
Kavish Devar
8cb2951bc6 ci: fix typo in release bundle asset name 2026-04-26 00:46:54 +05:30
Kavish Devar
bb578dab23 ci: upload artifacts separately 2026-04-26 00:43:09 +05:30
Kavish Devar
b1b47048a3 ci: fix keystore and add manual trigger 2026-04-26 00:29:14 +05:30
Hugo Holmqvist
bf09300dfe android: fix bypass_device_check.v2 being silently ignored (#543) 2026-04-26 00:21:58 +05:30
Kavish Devar
70165232c0 ci: fix ndk 2026-04-26 00:18:03 +05:30
Kavish Devar
aabbc902cb ci: release nightly builds on all changes 2026-04-25 23:58:52 +05:30
Kavish Devar
0ee7056600 android: fix versionName in builds 2026-04-25 23:58:23 +05:30
22 changed files with 1130 additions and 828 deletions

View File

@@ -1,4 +1,4 @@
name: Build APK and root module (and create nightly release) name: Android CI
on: on:
push: push:
@@ -6,95 +6,138 @@ on:
- '*' - '*'
paths: paths:
- 'android/**' - 'android/**'
pull_request:
paths:
- 'android/**'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release: branch:
description: 'Create a nightly release' description: Branch to build
required: true required: true
type: boolean default: main
default: false
custom_notes:
description: 'Custom updates to add to What''s Changed section'
required: false
type: string
workflow_call: workflow_call:
jobs: jobs:
build-debug-apk: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
short_sha: ${{ steps.vars.outputs.short_sha }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref }}
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: zulu
java-version: 21 java-version: 21
- uses: gradle/actions/setup-gradle@v4 - uses: gradle/actions/setup-gradle@v4
- name: Build debug APK - name: Decode keystore
run: ./gradlew assembleDebug run: echo "${{ secrets.RELEASE_KEYSTORE_FILE }}" | base64 --decode > android/release.keystore
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Accept Licenses
run: yes | sdkmanager --licenses
- name: Install NDK
run: sdkmanager "ndk;30.0.14904198"
- name: Create local.properties
run: |
cat <<EOF > android/local.properties
RELEASE_STORE_FILE=../release.keystore
RELEASE_STORE_PASSWORD=${{ secrets.RELEASE_STORE_PASSWORD }}
RELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }}
RELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }}
EOF
- name: Build
run: ./gradlew packageReleaseArtifacts
working-directory: android working-directory: android
- name: Upload artifact - id: vars
uses: actions/upload-artifact@v4 run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with: with:
name: Debug APK name: apk-release
path: android/app/build/outputs/apk/**/*.apk path: release/*release.apk
nightly-release:
- uses: actions/upload-artifact@v4
with:
name: apk-debug
path: release/*debug.apk
- uses: actions/upload-artifact@v4
with:
name: root-module-release
path: release/*release.zip
- uses: actions/upload-artifact@v4
with:
name: root-module-debug
path: release/*debug.zip
- uses: actions/upload-artifact@v4
with:
name: release-bundle
path: release/*.aab
release:
if: github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/release-nightly' || github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'true' needs: build
needs: build-debug-apk
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
- name: Export APK_NAME for later use with:
run: echo "APK_NAME=LibrePods-$(echo ${{ github.sha }} | cut -c1-7).apk" >> $GITHUB_ENV name: apk-release
- name: Rename .apk file path: artifacts/apk-release
run: mv "./Debug APK/debug/"*.apk "./$APK_NAME"
- name: Decode keystore file
run: echo "${{ secrets.DEBUG_KEYSTORE_FILE }}" | base64 --decode > debug.keystore
- name: Install apksigner
run: sudo apt-get update && sudo apt-get install -y apksigner
- name: Sign APK
run: |
apksigner sign --ks debug.keystore --ks-key-alias androiddebugkey --ks-pass pass:android --key-pass pass:android "./$APK_NAME"
- name: Verify APK
run: apksigner verify "./$APK_NAME"
- name: Fetch the latest non-nightly release tag
id: fetch-tag
run: echo "::set-output name=tag::$(git describe --tags $(git rev-list --tags --max-count=1))"
- name: Retrieve commits since the last release
id: get-commits
run: |
COMMITS=$(git log ${{ steps.fetch-tag.outputs.tag }}..HEAD --pretty=format:"- %s (%h)" --abbrev-commit)
echo "::set-output name=commits::${COMMITS}"
- name: Prepare release notes
id: release-notes
run: |
# Create a temporary file for release notes
NOTES_FILE=$(mktemp)
# Process custom notes if they exist - uses: actions/download-artifact@v4
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.custom_notes }}" ]; then with:
CUSTOM_NOTES="${{ github.event.inputs.custom_notes }}" name: apk-debug
path: artifacts/apk-debug
# Check if custom notes already have bullet points or GitHub-style formatting - uses: actions/download-artifact@v4
if echo "$CUSTOM_NOTES" | grep -q "^\*\|^- \|http.*commit\|in #[0-9]\+"; then with:
# Already formatted, use as is name: root-module-release
echo "$CUSTOM_NOTES" > "$NOTES_FILE" path: artifacts/root-module-release
else
# Add bullet point formatting
echo "- $CUSTOM_NOTES" > "$NOTES_FILE"
fi
fi
echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT - uses: actions/download-artifact@v4
- name: Zip root-module directory with:
run: sh ./build-magisk-module.sh name: root-module-debug
- name: Delete release if exist then create release path: artifacts/root-module-debug
- id: prev
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
gh release view "nightly" && gh release delete "nightly" -y --cleanup-tag TAG=$(gh release list \
gh release create "nightly" "./$APK_NAME" "./btl2capfix.zip" -p -t "Nightly Release" --notes-file "${{ steps.release-notes.outputs.notes_file }}" --generate-notes --limit 1 \
--json tagName \
--jq '.[0].tagName')
echo "tag=$TAG" >> $GITHUB_OUTPUT
- id: changelog
run: |
if [ -z "${{ steps.prev.outputs.tag }}" ]; then
NOTES=$(git log --pretty=format:"- %s (%h)")
else
NOTES=$(git log ${{ steps.prev.outputs.tag }}..HEAD --pretty=format:"- %s (%h)")
fi
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- id: tag
run: echo "tag=nightly-${{ needs.build.outputs.short_sha }}" >> $GITHUB_OUTPUT
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.tag.outputs.tag }}" \
artifacts/**/* \
-t "Nightly ${{ needs.build.outputs.short_sha }}" \
--notes "${{ steps.changelog.outputs.notes }}" \
--prerelease

View File

@@ -1,6 +1,6 @@
import java.util.Properties import java.util.Properties
val versionName = "0.2.3" val appVersionName = "0.2.5"
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
@@ -30,8 +30,8 @@ android {
applicationId = "me.kavishdevar.librepods" applicationId = "me.kavishdevar.librepods"
minSdk = 33 minSdk = 33
targetSdk = 37 targetSdk = 37
versionCode = 38 versionCode = 42
versionName = versionName versionName = appVersionName
} }
buildTypes { buildTypes {
release { release {
@@ -177,7 +177,7 @@ fun registerRootModuleZipTask(
rename { "LibrePods.apk" } rename { "LibrePods.apk" }
} }
archiveFileName.set("LibrePods-FOSS-v$versionName-$buildType.zip") archiveFileName.set("LibrePods-FOSS-v$appVersionName-$buildType.zip")
destinationDirectory.set(layout.buildDirectory.dir("outputs/rootModuleZips")) destinationDirectory.set(layout.buildDirectory.dir("outputs/rootModuleZips"))
} }
@@ -205,12 +205,12 @@ val collect = tasks.register<Copy>("collectReleaseArtifacts") {
from(layout.buildDirectory.dir("outputs/apk/xposed/release")) { from(layout.buildDirectory.dir("outputs/apk/xposed/release")) {
include("*.apk") include("*.apk")
rename(".*", "LibrePods-FOSS-v$versionName-release.apk") rename(".*", "LibrePods-FOSS-v$appVersionName-release.apk")
} }
from(layout.buildDirectory.dir("outputs/apk/xposed/debug")) { from(layout.buildDirectory.dir("outputs/apk/xposed/debug")) {
include("*.apk") include("*.apk")
rename(".*", "LibrePods-FOSS-v$versionName-debug.apk") rename(".*", "LibrePods-FOSS-v$appVersionName-debug.apk")
} }
from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) { from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) {

View File

@@ -65,6 +65,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
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.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Notifications
@@ -86,11 +87,13 @@ 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.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
@@ -118,11 +121,15 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.AirPodsNotifications
import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.presentation.components.AppInfoCard
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
import me.kavishdevar.librepods.presentation.components.PlayBypassSheet import me.kavishdevar.librepods.presentation.components.SelectItem
import me.kavishdevar.librepods.presentation.components.StyledBottomSheet
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.presentation.components.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledInputField
import me.kavishdevar.librepods.presentation.components.StyledSelectList
import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen
import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
@@ -146,6 +153,7 @@ import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
import me.kavishdevar.librepods.services.AirPodsService import me.kavishdevar.librepods.services.AirPodsService
import me.kavishdevar.librepods.utils.XposedState
import me.kavishdevar.librepods.utils.isSupported import me.kavishdevar.librepods.utils.isSupported
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -157,7 +165,7 @@ lateinit var connectionStatusReceiver: BroadcastReceiver
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
companion object { companion object {
init { init {
if (BuildConfig.FLAVOR == "xposed") { if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
System.loadLibrary("l2c_fcr_hook") System.loadLibrary("l2c_fcr_hook")
} }
} }
@@ -216,7 +224,7 @@ class MainActivity : ComponentActivity() {
fun Main() { fun Main() {
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (!isSupported(sharedPreferences)) { if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) {
val showDialog = remember { mutableStateOf(false) } val showDialog = remember { mutableStateOf(false) }
val showPlayBypassVisible = remember { mutableStateOf(false) } val showPlayBypassVisible = remember { mutableStateOf(false) }
val hazeState = rememberHazeState() val hazeState = rememberHazeState()
@@ -242,27 +250,25 @@ fun Main() {
verticalArrangement = Arrangement verticalArrangement = Arrangement
.spacedBy(16.dp) .spacedBy(16.dp)
) { ) {
val innerBackdrop = rememberLayerBackdrop()
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
Column( Column(
modifier = Modifier.layerBackdrop(innerBackdrop), modifier = Modifier,
verticalArrangement = Arrangement verticalArrangement = Arrangement
.spacedBy(16.dp) .spacedBy(16.dp)
) { ) {
Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(R.string.not_supported), text = stringResource(R.string.not_supported),
style = TextStyle( style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)), fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = textColor, color = textColor,
fontSize = 20.sp, fontSize = 28.sp,
textAlign = TextAlign.Center textAlign = TextAlign.Center
), ),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
DeviceInfoCard()
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -274,7 +280,7 @@ fun Main() {
style = TextStyle( style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)), fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) Color.White else Color.Black, color = if (isDarkTheme) Color.White else Color.Black,
fontSize = 16.sp fontSize = 16.sp
), ),
modifier = Modifier modifier = Modifier
@@ -282,22 +288,27 @@ fun Main() {
.padding(horizontal = 12.dp, vertical = 16.dp) .padding(horizontal = 12.dp, vertical = 16.dp)
) )
} }
} StyledButton(
StyledButton( onClick = { showDialog.value = true },
onClick = { showDialog.value = true }, backdrop = rememberLayerBackdrop(),
backdrop = innerBackdrop, modifier = Modifier
modifier = Modifier .fillMaxWidth(),
.fillMaxWidth() isInteractive = false,
) { surfaceColor = if (isDarkTheme) Color(0xFF862424) else Color(0xFFC94646)
Text( ) {
text = stringResource(R.string.bypass_compatibility_check), Text(
style = TextStyle( text = stringResource(R.string.bypass_compatibility_check),
fontFamily = FontFamily(Font(R.font.sf_pro)), style = TextStyle(
fontWeight = FontWeight.Medium, fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (isSystemInDarkTheme()) Color.White else Color.Black, fontWeight = FontWeight.Medium,
fontSize = 16.sp color = Color.White,
), fontSize = 16.sp
) ),
)
}
Spacer(modifier = Modifier.height(24.dp))
DeviceInfoCard()
AppInfoCard()
} }
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
} }
@@ -316,36 +327,132 @@ fun Main() {
} else { } else {
sharedPreferences.edit { sharedPreferences.edit {
putBoolean("bypass_device_check.v2", true) putBoolean("bypass_device_check.v2", true)
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
} }
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
} }
}, },
onDismiss = { onDismiss = {
showDialog.value = false showDialog.value = false
}, },
hazeState = hazeState backdrop = backdrop
// hazeState = hazeState
) )
if (BuildConfig.PLAY_BUILD) { if (BuildConfig.PLAY_BUILD) {
PlayBypassSheet( StyledBottomSheet(
visible = showPlayBypassVisible.value, visible = showPlayBypassVisible.value,
onDismiss = { onDismiss = {
showPlayBypassVisible.value = false showPlayBypassVisible.value = false
showDialog.value = true showDialog.value = true
}, },
onConfirm = {
showPlayBypassVisible.value = false
sharedPreferences.edit {
putBoolean("bypass_device_check.v2", true)
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}
},
backdrop = backdrop backdrop = backdrop
) ) { innerBackdrop, _ ->
val contentColor = if (isDarkTheme) Color.White else Color.Black
var acknowledged by remember { mutableStateOf(false) }
val inputState = rememberTextFieldState("")
val isValid = acknowledged && inputState.text.trim() == "OK"
val sfPro = FontFamily(Font(R.font.sf_pro))
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
text = stringResource(R.string.bypass_compatibility_check),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
color = contentColor
),
modifier = Modifier.padding(horizontal = 12.dp)
)
Text(
text = stringResource(R.string.compatibility_play_dialog_confirmation),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = contentColor
),
modifier = Modifier.padding(horizontal = 12.dp)
)
StyledSelectList(
items = listOf(
SelectItem(
name = stringResource(R.string.read_compatibility_requirements),
selected = acknowledged,
onClick = { acknowledged = !acknowledged }
)
)
)
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.show()
}
StyledInputField(
inputState = inputState,
focusRequester = focusRequester,
placeholder = stringResource(R.string.type_ok_to_continue, "OK")
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
StyledButton(
onClick = { showPlayBypassVisible.value = false },
backdrop = innerBackdrop,
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.no),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = contentColor
)
)
}
StyledButton(
onClick = {
showPlayBypassVisible.value = false
sharedPreferences.edit {
putBoolean("bypass_device_check.v2", true)
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}
},
backdrop = innerBackdrop,
isInteractive = isValid,
modifier = Modifier.weight(1f),
enabled = isValid,
surfaceColor = if (isDarkTheme) Color(0xFF0091FF) else Color(0xFF0088FF)
) {
Text(
text = stringResource(R.string.proceed),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = if (isValid) contentColor else contentColor.copy(alpha = 0.4f)
)
)
}
}
}
}
} }
return return

View File

@@ -0,0 +1,193 @@
/*
LibrePods - AirPods liberated from Apples 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.components
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
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.sp
import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R
@Composable
fun AppInfoCard() {
val rowHeight = remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
Column {
Box(
modifier = Modifier
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
.padding(start = 16.dp, bottom = 8.dp, end = 4.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))
)
)
}
Column(
modifier = Modifier
.clip(RoundedCornerShape(28.dp))
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(28.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))
)
)
}
}
}
}

View File

@@ -18,37 +18,32 @@
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.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
@@ -56,13 +51,13 @@ 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.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 com.kyant.backdrop.backdrops.LayerBackdrop
import androidx.compose.ui.window.DialogProperties import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.HazeState import com.kyant.backdrop.drawBackdrop
import dev.chrisbanes.haze.hazeEffect import com.kyant.backdrop.effects.blur
import dev.chrisbanes.haze.materials.CupertinoMaterials import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
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
@@ -75,162 +70,107 @@ fun ConfirmationDialog(
dismissText: String = "Cancel", dismissText: String = "Cancel",
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> Unit = { showDialog.value = false }, onDismiss: () -> Unit = { showDialog.value = false },
hazeState: HazeState, backdrop: LayerBackdrop,
) { ) {
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(0xFF0091FF) else Color(0xFF0088FF)
val haptics = LocalHapticFeedback.current AnimatedVisibility(
val scope = rememberCoroutineScope() visible = showDialog.value,
enter = scaleIn(initialScale = 1.05f) + fadeIn(),
if (showDialog.value) { exit = scaleOut(targetScale = 1.05f) + fadeOut()
Dialog( ) {
onDismissRequest = { showDialog.value = false }, Box(
properties = DialogProperties( modifier = Modifier.fillMaxSize(),
dismissOnBackPress = false, contentAlignment = Alignment.Center
dismissOnClickOutside = false
)
) { ) {
val innerBackdrop = rememberLayerBackdrop()
Box( Box(
modifier = Modifier modifier = Modifier
// .fillMaxWidth(0.75f) .fillMaxSize()
.requiredWidthIn(min = 200.dp, max = 360.dp) .background(Color.Black.copy(alpha = 0.4f))
.background(Color.Transparent, RoundedCornerShape(14.dp)) .clickable(enabled = false, onClick = {}),
.clip(RoundedCornerShape(14.dp)) contentAlignment = Alignment.Center
.hazeEffect(
hazeState,
style = CupertinoMaterials.regular(
containerColor = if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f)
)
)
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Box(
Spacer(modifier = Modifier.height(24.dp)) modifier = Modifier
Text( .requiredWidthIn(min = 200.dp, max = 360.dp)
title, .clip(RoundedCornerShape(48.dp))
style = TextStyle( .drawBackdrop(
fontSize = 16.sp, backdrop = backdrop,
fontWeight = FontWeight.Bold, exportedBackdrop = innerBackdrop,
color = textColor, shape = { RoundedCornerShape(48.dp) },
fontFamily = FontFamily(Font(R.font.sf_pro)) effects = {
), vibrancy()
textAlign = TextAlign.Center, blur(4f.dp.toPx())
modifier = Modifier.padding(horizontal = 16.dp) lens(12f.dp.toPx(), 48f.dp.toPx(), true)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
message,
style = TextStyle(
fontSize = 14.sp,
color = textColor.copy(alpha = 0.8f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(
thickness = 1.dp,
color = Color(0x40888888),
modifier = Modifier.fillMaxWidth()
)
var leftPressed by remember { mutableStateOf(false) }
var rightPressed by remember { mutableStateOf(false) }
val pressedColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
Row(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val position = event.changes.first().position
val width = size.width.toFloat()
val height = size.height.toFloat()
val isWithinBounds = position.y >= 0 && position.y <= height
val isLeft = position.x < width / 2
event.changes.first().consume()
when (event.type) {
PointerEventType.Press -> {
if (isWithinBounds) {
leftPressed = isLeft
rightPressed = !isLeft
} else {
leftPressed = false
rightPressed = false
}
}
PointerEventType.Move -> {
if (isWithinBounds) {
if (leftPressed != isLeft) scope.launch { haptics.performHapticFeedback(
HapticFeedbackType.SegmentTick) }
leftPressed = isLeft
rightPressed = !isLeft
} else {
leftPressed = false
rightPressed = false
}
}
PointerEventType.Release -> {
if (isWithinBounds) {
if (leftPressed) {
scope.launch { haptics.performHapticFeedback(
HapticFeedbackType.Reject) }
onDismiss()
} else if (rightPressed) {
scope.launch { haptics.performHapticFeedback(
HapticFeedbackType.Confirm) }
onConfirm()
}
}
leftPressed = false
rightPressed = false
}
}
}
}
}, },
horizontalArrangement = Arrangement.Start, onDrawSurface = {
verticalAlignment = Alignment.CenterVertically drawRect(
) { if (isDarkTheme) Color(0xFF1F1F1F).copy(alpha = 0.35f) else Color(0xFFE0E0E0).copy(alpha = 0.7f)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(if (leftPressed) pressedColor else Color.Transparent),
contentAlignment = Alignment.Center
) {
Text(
text = dismissText,
style = TextStyle(
color = accentColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
) )
) })) {
} Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( Spacer(modifier = Modifier.height(24.dp))
modifier = Modifier Text(
.width(1.dp) title,
.fillMaxHeight() style = TextStyle(
.background(Color(0x40888888)) fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
) )
Box( Spacer(modifier = Modifier.height(12.dp))
modifier = Modifier Text(
.weight(1f) message,
.fillMaxHeight() style = TextStyle(
.background(if (rightPressed) pressedColor else Color.Transparent), fontSize = 14.sp,
contentAlignment = Alignment.Center color = textColor.copy(alpha = 0.8f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(0.9f),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) { ) {
Text( StyledButton(
text = confirmText, onClick = onDismiss,
style = TextStyle( backdrop = innerBackdrop,
color = accentColor, modifier = Modifier.weight(1f),
fontFamily = FontFamily(Font(R.font.sf_pro)) ) {
Text(
text = dismissText, style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = textColor
)
) )
) }
StyledButton(
onClick = onConfirm,
backdrop = innerBackdrop,
modifier = Modifier.weight(1f),
surfaceColor = accentColor
) {
Text(
text = confirmText, style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = Color.White
)
)
}
} }
Spacer(modifier = Modifier.height(24.dp))
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import android.os.Build
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.Arrangement
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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -27,6 +28,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.utils.XposedState
@Composable @Composable
fun DeviceInfoCard() { fun DeviceInfoCard() {
@@ -41,14 +43,20 @@ fun DeviceInfoCard() {
Column ( Column (
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Box(
text = stringResource(R.string.device_info), style = TextStyle( modifier = Modifier
fontSize = 14.sp, .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
fontWeight = FontWeight.Bold, .padding(start = 16.dp, top = 24.dp, end = 4.dp)
color = textColor.copy(alpha = 0.6f), ) {
fontFamily = FontFamily(Font(R.font.sf_pro)) Text(
), modifier = Modifier.padding(start = 16.dp) text = stringResource(R.string.device_info), style = TextStyle(
) fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
Column( Column(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(28.dp)) .clip(RoundedCornerShape(28.dp))
@@ -166,6 +174,62 @@ fun DeviceInfoCard() {
) )
) )
} }
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.xposed_available), style = TextStyle(
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Text(
text = if (XposedState.isAvailable) stringResource(R.string.yes) else stringResource(R.string.no), 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.app_enabled_in_xposed), style = TextStyle(
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Text(
text = if (XposedState.bluetoothScopeEnabled) stringResource(R.string.yes) else stringResource(R.string.no), 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))
)
)
}
} }
} }
} }

View File

@@ -1,254 +0,0 @@
package me.kavishdevar.librepods.presentation.components
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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.sp
import com.kyant.backdrop.backdrops.LayerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R
@ExperimentalHazeMaterialsApi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlayBypassSheet(
visible: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
backdrop: LayerBackdrop
) {
if (!visible) return
val dark = isSystemInDarkTheme()
val contentColor = if (dark) Color.White else Color.Black
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var acknowledged by remember { mutableStateOf(false) }
val inputState = rememberTextFieldState("")
val isValid = acknowledged && inputState.text.trim() == "OK"
val sfPro = FontFamily(Font(R.font.sf_pro))
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color.Transparent,
dragHandle = { },
shape = RoundedCornerShape(48.dp),
scrimColor = Color.Transparent,
modifier = Modifier.padding(16.dp)
) {
val innerBackdrop = rememberLayerBackdrop()
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(48.dp))
.drawBackdrop(
backdrop = backdrop,
exportedBackdrop = innerBackdrop,
shape = { RoundedCornerShape(48.dp) },
effects = {
vibrancy()
blur(6f.dp.toPx())
lens(12f.dp.toPx(), 48f.dp.toPx(), true)
},
onDrawSurface = {
drawRect(
if (dark) Color.DarkGray.copy(alpha = 0.3f) else Color.White.copy(alpha = 0.6f)
)
}
)
.padding(24.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
text = stringResource(R.string.bypass_compatibility_check),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
color = contentColor
),
modifier = Modifier.padding(horizontal = 12.dp)
)
Text(
text = stringResource(R.string.compatibility_play_dialog_confirmation),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = contentColor
),
modifier = Modifier.padding(horizontal = 12.dp)
)
StyledSelectList(
items = listOf(
SelectItem(
name = stringResource(R.string.read_compatibility_requirements),
selected = acknowledged,
onClick = { acknowledged = !acknowledged }
)
)
)
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.show()
}
val backgroundColor = if (dark) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (dark) Color.White else Color.Black
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(58.dp)
.background(
backgroundColor,
RoundedCornerShape(28.dp)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
BasicTextField(
state = inputState,
textStyle = TextStyle(
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
cursorBrush = SolidColor(textColor),
decorator = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier
.weight(1f)
) {
Box {
if (inputState.text == "") {
Text(
text = stringResource(R.string.type_ok_to_continue, "OK"),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
fontFamily = sfPro,
color = textColor.copy(alpha = 0.8f)
)
)
}
innerTextField()
}
}
IconButton(
onClick = {
inputState.clearText()
}
) {
Text(
text = "􀁡",
style = TextStyle(
fontSize = 16.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (dark) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f)
),
)
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp)
.focusRequester(focusRequester)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
StyledButton(
onClick = onDismiss,
backdrop = innerBackdrop,
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.no),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = contentColor
)
)
}
StyledButton(
onClick = onConfirm,
backdrop = innerBackdrop,
isInteractive = isValid,
modifier = Modifier.weight(1f),
enabled = isValid,
surfaceColor = if (dark) Color(0xFF0091FF) else Color(0xFF0088FF)
) {
Text(
text = stringResource(R.string.proceed),
style = TextStyle(
fontFamily = sfPro,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = if (isValid) contentColor else contentColor.copy(alpha = 0.4f)
)
)
}
}
}
}
}
}

View File

@@ -0,0 +1,87 @@
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import com.kyant.backdrop.backdrops.LayerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StyledBottomSheet(
visible: Boolean,
onDismiss: () -> Unit,
backdrop: LayerBackdrop,
content: @Composable (innerBackdrop: LayerBackdrop, progress: Float) -> Unit
) {
if (!visible) return
val isDarkTheme = isSystemInDarkTheme()
val sheetState = rememberModalBottomSheetState(false) // move this to parent composable
val isExpanded = sheetState.targetValue == SheetValue.Expanded
val progress by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0f,
label = "sheetProgress"
)
val animatedCorner = lerp(48.dp, 42.dp, progress)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color.Transparent,
dragHandle = { },
shape = RoundedCornerShape(animatedCorner),
scrimColor = Color.Transparent,
modifier = Modifier.padding(4.dp)
) {
val innerBackdrop = rememberLayerBackdrop()
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(animatedCorner))
.drawBackdrop(
backdrop = backdrop,
exportedBackdrop = innerBackdrop,
shape = { RoundedCornerShape(animatedCorner) },
effects = {
vibrancy()
blur(4f.dp.toPx())
lens(12f.dp.toPx(), 48f.dp.toPx(), true)
},
onDrawSurface = {
drawRect(
if (isDarkTheme) Color.DarkGray.copy(alpha = 0.3f) else Color(
0xFFE0E0E0
).copy(alpha = 0.45f)
)
}
)
.padding(top = 24.dp)
.padding(horizontal = 16.dp)
) {
content(innerBackdrop, progress)
}
}
}

View File

@@ -52,6 +52,7 @@ 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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback 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
@@ -81,9 +82,11 @@ import kotlin.math.tanh
fun StyledIconButton( fun StyledIconButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: String, icon: String,
tint: Color = Color.Unspecified, iconTint: Color = Color.Unspecified,
surfaceColor: Color = Color.Unspecified,
backdrop: LayerBackdrop = rememberLayerBackdrop(), backdrop: LayerBackdrop = rememberLayerBackdrop(),
onClick: () -> Unit onClick: () -> Unit,
enabled: Boolean = true
) { ) {
val haptics = LocalHapticFeedback.current val haptics = LocalHapticFeedback.current
val darkMode = isSystemInDarkTheme() val darkMode = isSystemInDarkTheme()
@@ -96,6 +99,7 @@ fun StyledIconButton(
val innerShadowLayer = rememberGraphicsLayer().apply { val innerShadowLayer = rememberGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.Offscreen compositingStrategy = CompositingStrategy.Offscreen
} }
val density = LocalDensity.current
val interactiveHighlightShader = remember { val interactiveHighlightShader = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -120,8 +124,10 @@ half4 main(float2 coord) {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
TextButton( TextButton(
onClick = { onClick = {
scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) } if (enabled) {
onClick() scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
onClick()
}
}, },
shape = RoundedCornerShape(56.dp), shape = RoundedCornerShape(56.dp),
modifier = modifier modifier = modifier
@@ -137,6 +143,7 @@ half4 main(float2 coord) {
) )
}, },
layerBlock = { layerBlock = {
if (!enabled) return@drawBackdrop
val width = size.width val width = size.width
val height = size.height val height = size.height
@@ -161,6 +168,12 @@ half4 main(float2 coord) {
(height / width).fastCoerceAtMost(1f) (height / width).fastCoerceAtMost(1f)
}, },
onDrawSurface = { onDrawSurface = {
if (!enabled) {
drawRect(
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(0.5f)
)
return@drawBackdrop
}
val progress = progressAnimation.value.coerceIn(0f, 1f) val progress = progressAnimation.value.coerceIn(0f, 1f)
val shape = RoundedCornerShape(56.dp) val shape = RoundedCornerShape(56.dp)
@@ -187,6 +200,10 @@ half4 main(float2 coord) {
} }
drawLayer(innerShadowLayer) drawLayer(innerShadowLayer)
if (surfaceColor.isSpecified) {
drawRect(surfaceColor)
}
drawRect( drawRect(
(if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy( (if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(
progress.coerceIn( progress.coerceIn(
@@ -197,6 +214,7 @@ half4 main(float2 coord) {
) )
}, },
onDrawFront = { onDrawFront = {
if (!enabled) return@drawBackdrop
val progress = progressAnimation.value.fastCoerceIn(0f, 1f) val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
if (progress > 0f) { if (progress > 0f) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
@@ -241,40 +259,46 @@ half4 main(float2 coord) {
) )
.pointerInput(scope) { .pointerInput(scope) {
val onDragStop: () -> Unit = { val onDragStop: () -> Unit = {
scope.launch { if (enabled) {
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) } scope.launch {
launch { progressAnimation.animateTo(0f, progressAnimationSpec) } launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) } launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
}
} }
} }
inspectDragGestures( inspectDragGestures(
onDragStart = { down -> onDragStart = { down ->
pressStartPosition = down.position if (enabled) {
scope.launch { pressStartPosition = down.position
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) } scope.launch {
launch { progressAnimation.animateTo(1f, progressAnimationSpec) } launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
launch { offsetAnimation.snapTo(Offset.Zero) } launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
launch { offsetAnimation.snapTo(Offset.Zero) }
}
} }
}, },
onDragEnd = { onDragStop() }, onDragEnd = { onDragStop() },
onDragCancel = onDragStop onDragCancel = onDragStop
) { _, dragAmount -> ) { _, dragAmount ->
scope.launch { scope.launch {
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback( if (enabled) {
HapticFeedbackType.SegmentFrequentTick if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
) HapticFeedbackType.SegmentFrequentTick
offsetAnimation.snapTo(offsetAnimation.value + dragAmount) )
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
}
} }
} }
} }
.size(48.dp), .size(with(density) { 48.sp.toDp() }),
) { ) {
Text( Text(
text = icon, text = icon,
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 20.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = if (tint.isSpecified) tint else if (darkMode) Color.White else Color.Black, color = if (iconTint.isSpecified) iconTint else if (darkMode) Color.White else Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
) )
) )

View File

@@ -0,0 +1,154 @@
package me.kavishdevar.librepods.presentation.components
import android.R.attr.singleLine
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
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.sp
import me.kavishdevar.librepods.R
@Composable
fun StyledInputField(
inputState: TextFieldState,
focusRequester: FocusRequester,
placeholder: String = "",
singleLine: Boolean = true
){
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val minHeight = if (singleLine) 58.dp else 120.dp
val verticalAlignment = if (singleLine) Alignment.CenterVertically else Alignment.Top
val hasText = inputState.text.isNotEmpty()
val density = LocalDensity.current
val spacerHeight by animateDpAsState(
targetValue = if (hasText) with(density) { 32.sp.toDp() } else 0.dp,
label = "labelSpacer"
)
val transition = updateTransition(hasText, label = "floating")
val yOffset by transition.animateDp(label = "y") {
if (it) with (density) { (-48).sp.toDp() } else 0.dp
}
Spacer(modifier = Modifier.height(spacerHeight))
Box(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = verticalAlignment,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = minHeight)
.background(
backgroundColor,
RoundedCornerShape(28.dp)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
.pointerInput(Unit) {
detectTapGestures {
focusRequester.requestFocus()
}
}
) {
BasicTextField(
state = inputState,
lineLimits = if (singleLine) TextFieldLineLimits.SingleLine else TextFieldLineLimits.Default,
textStyle = TextStyle(
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
cursorBrush = SolidColor(textColor),
decorator = { innerTextField ->
Row(
modifier = Modifier.padding(top = if (singleLine) 0.dp else 16.dp),
verticalAlignment = verticalAlignment,
) {
Row(
modifier = Modifier
.weight(1f)
) {
Box(
modifier = Modifier
.weight(1f),
contentAlignment = if (singleLine) Alignment.CenterStart else Alignment.TopStart
) {
Text(
text = placeholder,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Light,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = textColor.copy(alpha = 0.8f)
),
modifier = Modifier
.offset(y = yOffset)
)
innerTextField()
}
}
if (singleLine && !inputState.text.isEmpty()) {
IconButton(
onClick = {
inputState.clearText()
}
) {
Text(
text = "􀁡",
style = TextStyle(
fontSize = 16.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
alpha = 0.6f
)
),
)
}
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp)
.focusRequester(focusRequester)
)
}
}
}

View File

@@ -28,7 +28,7 @@ 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.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -98,7 +98,7 @@ fun StyledSelectList(
Row( Row(
modifier = Modifier modifier = Modifier
.height(if (hasIcon) 72.dp else 55.dp) .heightIn(min = if (hasIcon) 72.dp else 55.dp)
.background(animatedBackgroundColor, shape) .background(animatedBackgroundColor, shape)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
@@ -128,7 +128,7 @@ fun StyledSelectList(
contentDescription = "Icon", contentDescription = "Icon",
tint = Color(0xFF007AFF), tint = Color(0xFF007AFF),
modifier = Modifier modifier = Modifier
.height(48.dp) .heightIn(min = 48.dp)
.wrapContentWidth() .wrapContentWidth()
) )
} }

View File

@@ -402,7 +402,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
} }
} }
// if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") { // if (!hearingAidEnabled && XposedState.isAvailable) {
// Text( // Text(
// text = stringResource(R.string.apply_eq_to), style = TextStyle( // text = stringResource(R.string.apply_eq_to), style = TextStyle(
// fontSize = 14.sp, // fontSize = 14.sp,

View File

@@ -25,6 +25,7 @@ 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.Arrangement
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.Spacer
@@ -35,8 +36,11 @@ import androidx.compose.foundation.layout.padding
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.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -49,12 +53,12 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.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.focus.FocusRequester
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
@@ -62,7 +66,9 @@ 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.input.KeyboardCapitalization 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -72,14 +78,20 @@ 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.presentation.components.AppInfoCard
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
import me.kavishdevar.librepods.presentation.components.NavigationButton import me.kavishdevar.librepods.presentation.components.NavigationButton
import me.kavishdevar.librepods.presentation.components.StyledBottomSheet
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledInputField
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider import me.kavishdevar.librepods.presentation.components.StyledSlider
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
import me.kavishdevar.librepods.utils.XposedState
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSettingsScreen( fun AppSettingsScreen(
navController: NavController, viewModel: AppSettingsViewModel = viewModel() navController: NavController, viewModel: AppSettingsViewModel = viewModel()
@@ -90,6 +102,12 @@ fun AppSettingsScreen(
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
val contactBottomSheet = remember { mutableStateOf(false) }
val subjectState = remember { TextFieldState() }
val descriptionState = remember { TextFieldState() }
val subjectFocusRequester = remember { FocusRequester() }
val descriptionFocusRequester = remember { FocusRequester() }
StyledScaffold( StyledScaffold(
title = stringResource(R.string.settings) title = stringResource(R.string.settings)
) { topPadding, hazeState, bottomPadding -> ) { topPadding, hazeState, bottomPadding ->
@@ -367,24 +385,28 @@ fun AppSettingsScreen(
independent = true, independent = true,
enabled = state.isPremium enabled = state.isPremium
) )
Spacer(modifier = Modifier.height(16.dp))
} else { } else {
Text( Box(
text = stringResource(R.string.customizations_unavailable),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = textColor.copy(alpha = 0.6f),
),
modifier = Modifier modifier = Modifier
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 16.dp) .padding(top = 16.dp, bottom = 2.dp)
) ) {
Text(
text = stringResource(R.string.customizations_unavailable),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = textColor.copy(alpha = 0.6f),
),
modifier = Modifier
)
}
} }
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
if (BuildConfig.FLAVOR == "xposed") {
Spacer(modifier = Modifier.height(16.dp))
val restartBluetoothText = val restartBluetoothText =
stringResource(R.string.found_offset_restart_bluetooth) stringResource(R.string.found_offset_restart_bluetooth)
StyledToggle( StyledToggle(
@@ -417,14 +439,20 @@ fun AppSettingsScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Box(
text = stringResource(R.string.contact), style = TextStyle( modifier = Modifier
fontSize = 14.sp, .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7))
fontWeight = FontWeight.Bold, .padding(start = 16.dp, bottom = 2.dp, top = 24.dp, end = 4.dp)
color = textColor.copy(alpha = 0.6f), ) {
fontFamily = FontFamily(Font(R.font.sf_pro)) Text(
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp) 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))
)
)
}
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Column( Column(
@@ -439,29 +467,7 @@ fun AppSettingsScreen(
to = "", to = "",
name = stringResource(R.string.email), name = stringResource(R.string.email),
navController = navController, navController = navController,
onClick = { onClick = { contactBottomSheet.value = true },
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:".toUri()
putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz"))
putExtra(Intent.EXTRA_SUBJECT, "LibrePods: <SUBJECT>")
putExtra(
Intent.EXTRA_TEXT,
"Describe your issue here:" +
"\n\n\n\n----------" +
"\nPhone details:" +
"\nMANUFACTURER: ${Build.MANUFACTURER}" +
"\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" +
"\nDISPLAY_VERSION: ${Build.DISPLAY} (${Build.PRODUCT})" +
"\nID: ${Build.ID} (SDK ${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 independent = false
) )
@@ -506,139 +512,8 @@ fun AppSettingsScreen(
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
DeviceInfoCard() DeviceInfoCard()
Spacer(modifier = Modifier.height(16.dp))
Text( AppInfoCard()
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(start = 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))
@@ -719,5 +594,94 @@ fun AppSettingsScreen(
}) })
} }
} }
StyledBottomSheet(
visible = contactBottomSheet.value,
onDismiss = { contactBottomSheet.value = false },
backdrop = backdrop
) { innerBackdrop, progress ->
val animatedPadding = lerp(16.dp, 2.dp, progress)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = animatedPadding)
.padding(bottom = 16.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
StyledIconButton(
icon = "\uDBC0\uDD84",
backdrop = innerBackdrop,
onClick = { contactBottomSheet.value = false }
)
Text (
text = stringResource(R.string.describe_your_issue),
style = TextStyle(
fontSize = 18.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = if (isSystemInDarkTheme()) Color.White else Color.Black
)
)
StyledIconButton(
icon = "\uDBC0\uDE1F",
backdrop = innerBackdrop,
surfaceColor = if (isSystemInDarkTheme()) Color(0xFF0091FF) else Color(0xFF0088FF),
iconTint = if (subjectState.text.isNotEmpty() && descriptionState.text.isNotEmpty()) Color.White else Color.Gray,
enabled = subjectState.text.isNotEmpty() && descriptionState.text.isNotEmpty(),
onClick = {
contactBottomSheet.value = false
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:".toUri()
putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz"))
putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ${subjectState.text}")
putExtra(
Intent.EXTRA_TEXT,
"${descriptionState.text}" +
"\n\n----------" +
"\nPhone details:" +
"\nMANUFACTURER: ${Build.MANUFACTURER}" +
"\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" +
"\nDISPLAY_VERSION: ${Build.DISPLAY}" +
"\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" +
"\nXposed enabled/active: ${XposedState.isAvailable}/${XposedState.bluetoothScopeEnabled}" +
"\n\nApp details:" +
"\nVERSION: ${BuildConfig.VERSION_NAME}" +
"\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" +
"\nFLAVOR: ${BuildConfig.FLAVOR}" +
"\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}"
)
}
context.startActivity(intent)
subjectState.clearText()
descriptionState.clearText()
}
)
}
Spacer(modifier = Modifier.height(8.dp))
StyledInputField(
inputState = subjectState,
focusRequester = subjectFocusRequester,
placeholder = stringResource(R.string.subject),
)
Spacer(modifier = Modifier.height(12.dp))
StyledInputField(
inputState = descriptionState,
focusRequester = descriptionFocusRequester,
placeholder = stringResource(R.string.describe_your_issue),
singleLine = false
)
}
}
} }
} }

View File

@@ -270,7 +270,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
hearingAidEnabled.value = false hearingAidEnabled.value = false
showDialog.value = false showDialog.value = false
}, },
hazeState = hazeStateS.value, // hazeState = hazeStateS.value,
// backdrop = backdrop backdrop = backdrop
) )
} }

View File

@@ -21,43 +21,26 @@
package me.kavishdevar.librepods.presentation.screens package me.kavishdevar.librepods.presentation.screens
import android.content.Context import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
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.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
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.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit import androidx.core.content.edit
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.presentation.components.StyledInputField
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -67,14 +50,12 @@ import kotlin.io.encoding.ExperimentalEncodingApi
@Composable @Composable
fun RenameScreen(viewModel: AirPodsViewModel) { fun RenameScreen(viewModel: AirPodsViewModel) {
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
val name = remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", "") ?: "")) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()
keyboardController?.show() keyboardController?.show()
name.value = name.value.copy(selection = TextRange(name.value.text.length))
} }
StyledScaffold( StyledScaffold(
@@ -86,67 +67,18 @@ fun RenameScreen(viewModel: AirPodsViewModel) {
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Spacer(modifier = Modifier.height(spacerHeight)) Spacer(modifier = Modifier.height(spacerHeight))
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) val textFieldState = rememberTextFieldState()
val textColor = if (isDarkTheme) Color.White else Color.Black textFieldState.edit { sharedPreferences.getString("name", "") ?: "" }
val cursorColor = if (isDarkTheme) Color.White else Color.Black LaunchedEffect(textFieldState.text) {
Row( sharedPreferences.edit {putString("name", textFieldState.text as String?)}
verticalAlignment = Alignment.CenterVertically, viewModel.setName(textFieldState.text.toString())
modifier = Modifier
.fillMaxWidth()
.height(58.dp)
.background(
backgroundColor,
RoundedCornerShape(28.dp)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
BasicTextField(
value = name.value,
onValueChange = {
name.value = it
sharedPreferences.edit {putString("name", it.text)}
viewModel.setName(it.text)
},
textStyle = TextStyle(
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
singleLine = true,
cursorBrush = SolidColor(cursorColor),
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier
.weight(1f)
) {
innerTextField()
}
IconButton(
onClick = {
name.value = TextFieldValue("")
}
) {
Text(
text = "􀁡",
style = TextStyle(
fontSize = 16.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f)
),
)
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp)
.focusRequester(focusRequester)
)
} }
StyledInputField(
textFieldState,
focusRequester
)
} }
} }
} }

View File

@@ -10,10 +10,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.data.XposedRemotePrefProvider
import me.kavishdevar.librepods.utils.NativeBridge import me.kavishdevar.librepods.utils.NativeBridge
import me.kavishdevar.librepods.utils.XposedState
import kotlin.math.roundToInt import kotlin.math.roundToInt
data class AppSettingsUiState( data class AppSettingsUiState(
@@ -91,7 +91,7 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false) connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
) )
} }
if (BuildConfig.FLAVOR == "xposed") { if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
NativeBridge.setSdpHook(_uiState.value.vendorIdHook) NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
} }
} }

View File

@@ -24,6 +24,9 @@ import android.os.Build
fun isSupported(sharedPreferences: SharedPreferences): Boolean { fun isSupported(sharedPreferences: SharedPreferences): Boolean {
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")
val isBypassFlagActive = sharedPreferences.getBoolean("bypass_device_check.v2", false)
if (isBypassFlagActive) return true
if (isPixel) { if (isPixel) {
when (Build.VERSION.SDK_INT) { when (Build.VERSION.SDK_INT) {
@@ -38,5 +41,5 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
} else if (isOppoOrOnePlus) { } else if (isOppoOrOnePlus) {
return Build.VERSION.SDK_INT >= 36 return Build.VERSION.SDK_INT >= 36
} }
return sharedPreferences.getBoolean("bypass_device_check.v2", false) return false
} }

View File

@@ -0,0 +1,6 @@
package me.kavishdevar.librepods.utils
object XposedState {
var isAvailable: Boolean = false
var bluetoothScopeEnabled: Boolean = false
}

View File

@@ -263,4 +263,8 @@
<string name="digital_assistant_on_long_press">Digital Assistant on Long Press</string> <string name="digital_assistant_on_long_press">Digital Assistant on Long Press</string>
<string name="digital_assistant_on_long_press_description">Invoke Digital Assistant when long pressing the AirPods Pro stem.</string> <string name="digital_assistant_on_long_press_description">Invoke Digital Assistant when long pressing the AirPods Pro stem.</string>
<string name="customizations_unavailable">Customizations unavailable. Connect your AirPods at least once to access.</string> <string name="customizations_unavailable">Customizations unavailable. Connect your AirPods at least once to access.</string>
<string name="xposed_available">Xposed available</string>
<string name="app_enabled_in_xposed">App enabled in Xposed</string>
<string name="subject">Subject</string>
<string name="describe_your_issue">Describe your issue</string>
</resources> </resources>

View File

@@ -9,6 +9,7 @@ import io.github.libxposed.service.XposedServiceHelper
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.utils.XposedServiceHolder import me.kavishdevar.librepods.utils.XposedServiceHolder
import me.kavishdevar.librepods.utils.XposedState
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver { class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {
override fun onCreate() { override fun onCreate() {
@@ -22,13 +23,18 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
BillingManager.provider.queryPurchases() BillingManager.provider.queryPurchases()
XposedState.isAvailable = true
XposedState.bluetoothScopeEnabled = XposedServiceHolder.service?.scope?.contains("com.google.android.bluetooth") == true || XposedServiceHolder.service?.scope?.contains("com.android.bluetooth") == true
} }
override fun onServiceBind(p0: XposedService) { override fun onServiceBind(service: XposedService) {
XposedServiceHolder.service = p0 XposedServiceHolder.service = service
XposedState.isAvailable = true
XposedState.bluetoothScopeEnabled = XposedServiceHolder.service?.scope?.contains("com.google.android.bluetooth") == true || XposedServiceHolder.service?.scope?.contains("com.android.bluetooth") == true
} }
override fun onServiceDied(p0: XposedService) { override fun onServiceDied(p0: XposedService) {
XposedServiceHolder.service = null XposedServiceHolder.service = null
XposedState.isAvailable = false
} }
} }

View File

@@ -20,6 +20,7 @@ class KotlinModule: XposedModule() {
log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion") log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
} }
@SuppressLint("UnsafeDynamicallyLoadedCode")
override fun onPackageLoaded(param: PackageLoadedParam) { override fun onPackageLoaded(param: PackageLoadedParam) {
log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}") log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
@@ -27,8 +28,36 @@ class KotlinModule: XposedModule() {
log(Log.INFO, 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(Log.INFO, TAG, "Loading native library for Bluetooth hook") val abi = android.os.Build.SUPPORTED_ABIS.first()
System.loadLibrary("l2c_fcr_hook") val soName = "libl2c_fcr_hook.so"
val candidates = buildList {
add("${moduleApplicationInfo.sourceDir}!/lib/$abi/$soName")
moduleApplicationInfo.splitSourceDirs?.forEach { split ->
add("$split!/lib/$abi/$soName")
}
}
var loaded = false
for (path in candidates) {
try {
log(Log.INFO, TAG, "Trying to load native lib from $path")
System.load(path)
log(Log.INFO, TAG, "Loaded native lib from $path")
loaded = true
break
} catch (e: Throwable) {
log(Log.WARN, TAG, "Failed to load from $path: ${e.message}")
}
}
if (!loaded) {
log(Log.ERROR, TAG, "Could not load $soName from base or splits")
return
}
val remotePrefValue = getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false) val remotePrefValue = getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false)
log(Log.INFO, TAG, "sdp hook enabled (remote pref): $remotePrefValue") log(Log.INFO, TAG, "sdp hook enabled (remote pref): $remotePrefValue")
NativeBridge.setSdpHook(remotePrefValue) NativeBridge.setSdpHook(remotePrefValue)