mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-05-13 08:02:17 +00:00
Compare commits
23 Commits
nightly-4e
...
nightly-5b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc5079e13 | ||
|
|
64d233d427 | ||
|
|
ea2c2b811b | ||
|
|
6f28df734e | ||
|
|
c15e15a6b7 | ||
|
|
75a52cdfd7 | ||
|
|
d1b32d5a00 | ||
|
|
044aff731f | ||
|
|
216c97f9ca | ||
|
|
fd3774b513 | ||
|
|
b7336940e6 | ||
|
|
b2ba830a80 | ||
|
|
f08769e62f | ||
|
|
d1933c3b67 | ||
|
|
fb44f01ac0 | ||
|
|
93a93cbe68 | ||
|
|
a4898293b8 | ||
|
|
845f26192c | ||
|
|
3321bb1c43 | ||
|
|
c7a5cb2d8c | ||
|
|
7b81411417 | ||
|
|
d80f2275a1 | ||
|
|
795bebc6ae |
110
.github/ISSUE_TEMPLATE/01-bug-report-android.yml
vendored
Normal file
110
.github/ISSUE_TEMPLATE/01-bug-report-android.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
name: Bug report (Android)
|
||||
description: Report a bug in the Android app
|
||||
labels: ["bug", "android"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug. Please fill in as much as you can.
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: App version
|
||||
description: "Find this in `Settings → About → Version` in the app, or in your phone's app info."
|
||||
placeholder: "v0.2.5 (build 46)"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: app-source
|
||||
attributes:
|
||||
label: App source
|
||||
options:
|
||||
- GitHub
|
||||
- Play
|
||||
- Built from source
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: Manufacturer and model.
|
||||
placeholder: "Google Pixel 8 Pro"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android / OS version
|
||||
description: Include the OEM skin if relevant.
|
||||
placeholder: "Android 16, OxygenOS 16, ColorOS 16, ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: root-method
|
||||
attributes:
|
||||
label: Root / hook method
|
||||
options:
|
||||
- No root (native L2CAP support)
|
||||
- Magisk + Xposed
|
||||
- KernelSU + Xposed
|
||||
- Other (describe in additional context)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: airpods-model
|
||||
attributes:
|
||||
label: AirPods model
|
||||
options:
|
||||
- AirPods (1st gen)
|
||||
- AirPods (2nd gen)
|
||||
- AirPods (3rd gen)
|
||||
- AirPods (4th gen)
|
||||
- AirPods (4th gen) with ANC
|
||||
- AirPods Pro (1st gen)
|
||||
- AirPods Pro 2 (Lightning)
|
||||
- AirPods Pro 2 (USB-C)
|
||||
- AirPods Pro 3
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: firmware
|
||||
attributes:
|
||||
label: AirPods firmware
|
||||
description: Find this under `About` in the app once connected.
|
||||
placeholder: "8454768"
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What happened
|
||||
description: Describe what you observed and what you expected. Include steps to reproduce if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
If you are rooted, give the app root access, open the app, go to `Settings → Troubleshooting → Collect Logs`, and attach the resulting file here.
|
||||
|
||||
Without logs most bugs are very hard to diagnose. If you are not, follow these instructions:
|
||||
(Needs access to a computer, and USB/Wireless Debugging under developer options enabled)
|
||||
|
||||
Commands:
|
||||
- Get the uid: Linux/Mac: `adb shell dumpsys package me.kavishdevar.librepods | grep uid`
|
||||
- Start logs: `adb logcat --uid=<uid>,1002 >> librepods-logs.txt` (1002 is for bluetooth)
|
||||
|
||||
Steps for proper logs
|
||||
- force close the app
|
||||
- turn off bluetooth
|
||||
- start logs
|
||||
- open the app
|
||||
- turn on bluetooth and connect
|
||||
|
||||
placeholder: Paste log content or attach the file
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Anything else that might help (screenshots, video, related issues, what you've already tried).
|
||||
83
.github/ISSUE_TEMPLATE/02-bug-report-linux.yml
vendored
Normal file
83
.github/ISSUE_TEMPLATE/02-bug-report-linux.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Bug report (Linux)
|
||||
description: Report a bug in the Linux program
|
||||
labels: ["bug", "linux"]
|
||||
title: "[Linux] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the report. Please fill in as much as you can.
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: App version
|
||||
placeholder: "linux-v0.1.0, or linux-rust commit abc1234"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: variant
|
||||
attributes:
|
||||
label: Variant
|
||||
options:
|
||||
- Rust rewrite (`linux-rust` branch)
|
||||
- QT version (NOT MAINTAINED! issues will be closed)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: distro
|
||||
attributes:
|
||||
label: Distro and version
|
||||
placeholder: "Arch Linux, Fedora 41, Ubuntu 24.04, NixOS 25.05"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: desktop
|
||||
attributes:
|
||||
label: Desktop environment / compositor
|
||||
placeholder: "GNOME 47 (Wayland), KDE 6 (X11), Hyprland, ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Install method (only official sources)
|
||||
options:
|
||||
- Built from source (`nix` or otherwise)
|
||||
- Pre-built binary
|
||||
- AppImage
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: airpods-model
|
||||
attributes:
|
||||
label: AirPods model
|
||||
options:
|
||||
- AirPods (1st gen)
|
||||
- AirPods (2nd gen)
|
||||
- AirPods (3rd gen)
|
||||
- AirPods (4th gen)
|
||||
- AirPods (4th gen) with ANC
|
||||
- AirPods Pro (1st gen)
|
||||
- AirPods Pro 2 (Lightning)
|
||||
- AirPods Pro 2 (USB-C)
|
||||
- AirPods Pro 3
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What happened
|
||||
description: Describe what you observed and what you expected. Include steps to reproduce if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs and stderr
|
||||
description: Run the app from a terminal with `--debug` and paste the output.
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Additional context
|
||||
31
.github/ISSUE_TEMPLATE/03-feature-request.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/03-feature-request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature or improvement
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: Scope
|
||||
options:
|
||||
- Android
|
||||
- Linux
|
||||
- Both
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem or use case
|
||||
description: What are you trying to do? What is missing or hard today?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: How might it work? UI sketches, behavior, edge cases.
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
21
.github/workflows/ci-android.yml
vendored
21
.github/workflows/ci-android.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- '*'
|
||||
paths:
|
||||
- 'android/**'
|
||||
- 'root-module-manual/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'android/**'
|
||||
@@ -34,6 +35,7 @@ jobs:
|
||||
java-version: 21
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
- name: Decode keystore
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "${{ secrets.RELEASE_KEYSTORE_FILE }}" | base64 --decode > android/release.keystore
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
@@ -42,6 +44,7 @@ jobs:
|
||||
- name: Install NDK
|
||||
run: sdkmanager "ndk;30.0.14904198"
|
||||
- name: Create local.properties
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
cat <<EOF > android/local.properties
|
||||
RELEASE_STORE_FILE=../release.keystore
|
||||
@@ -49,7 +52,12 @@ jobs:
|
||||
RELEASE_KEY_ALIAS=${{ secrets.RELEASE_KEY_ALIAS }}
|
||||
RELEASE_KEY_PASSWORD=${{ secrets.RELEASE_KEY_PASSWORD }}
|
||||
EOF
|
||||
- name: Build
|
||||
- name: Build debug APK for PRs
|
||||
if: github.event_name == 'pull_request'
|
||||
run: ./gradlew assembleFossDebug
|
||||
working-directory: android
|
||||
- name: Build release artifacts
|
||||
if: github.event_name != 'pull_request'
|
||||
run: ./gradlew packageReleaseArtifacts
|
||||
working-directory: android
|
||||
- name: Get app version
|
||||
@@ -58,26 +66,37 @@ jobs:
|
||||
- id: vars
|
||||
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: apk-release
|
||||
path: release/*release.apk
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
name: apk-debug
|
||||
path: android/app/build/outputs/apk/foss/debug/app-foss-debug.apk
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: apk-debug
|
||||
path: release/*debug.apk
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: root-module-release
|
||||
path: release/*release.zip
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: root-module-debug
|
||||
path: release/*debug.zip
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: release-bundle
|
||||
path: release/*.aab
|
||||
|
||||
12
README.md
12
README.md
@@ -76,19 +76,15 @@ https://github.com/user-attachments/assets/43911243-0576-4093-8c55-89c1db5ea533
|
||||
|
||||
### Root Requirement
|
||||
|
||||
The app needs root because of a bug in the Android Bluetooth stack Fluoride/non-compliance of Apple with Bluetooth standards. You must have Xposed installed for the app to workaround this bug and connect to AirPods.
|
||||
LibrePods **may** require root depending on your device/OS and what features you want access to:
|
||||
|
||||
[https://issuetracker.google.com/issues/371713238](https://issuetracker.google.com/issues/371713238)
|
||||
|
||||
Please do not comment in the thread. The issue has already been resolved and should be available in Android 17 for all devices.
|
||||
|
||||
However, if you are using ColorOS/OxygenOS 16, Android 16 QPR3 on Pixel (ensure you're on the latest Play system update), you don't need root for most features.
|
||||
- Features requiring the VendorID hook ([the features marked with an asterisk here](https://github.com/kavishdevar/librepods#key-features)) will always require root regardless of your device/OS.
|
||||
- On **ColorOS/OxygenOS 16** and **Pixel devices on Android 16 QPR3** (with the latest Google Play system update), LibrePods does not need root for most features (except those requiring the VendorID hook mentioned above).
|
||||
- On other devices, LibrePods needs root because of a bug in the Android Bluetooth stack Fluoride/non-compliance of Apple with Bluetooth standards. You must have Xposed installed for the app to workaround this bug and connect to AirPods. [This issue is being tracked here](https://issuetracker.google.com/issues/371713238). Please do not comment on the issue thread. The issue has already been resolved and should be available in **Android 17** for all devices.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This workaround with Xposed is not guaranteed to work on all devices.
|
||||
|
||||
Features requiring the VendorID hook will still require root. These features include customizing transparency mode, setting up hearing aid, and use Bluetooth Multipoint.
|
||||
|
||||
### Troubleshooting steps for common errors
|
||||
- Ensure the correct scope is set in LSPosed/Vector.
|
||||
- Ensure there is no root-hiding module preventing the hook from loading on the Bluetooth app.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import java.util.Properties
|
||||
|
||||
val appVersionName = "0.2.6"
|
||||
val appVersionName = "0.2.9"
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -10,17 +10,29 @@ plugins {
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
val localPropsFile = rootProject.file("local.properties")
|
||||
val props = Properties().apply {
|
||||
load(rootProject.file("local.properties").inputStream())
|
||||
if (localPropsFile.exists()) {
|
||||
load(localPropsFile.inputStream())
|
||||
}
|
||||
}
|
||||
|
||||
val releaseSigningAvailable = listOf(
|
||||
"RELEASE_STORE_FILE",
|
||||
"RELEASE_STORE_PASSWORD",
|
||||
"RELEASE_KEY_ALIAS",
|
||||
"RELEASE_KEY_PASSWORD"
|
||||
).all { props[it]?.toString()?.isNotBlank() == true }
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(props["RELEASE_STORE_FILE"] as String)
|
||||
storePassword = props["RELEASE_STORE_PASSWORD"] as String
|
||||
keyAlias = props["RELEASE_KEY_ALIAS"] as String
|
||||
keyPassword = props["RELEASE_KEY_PASSWORD"] as String
|
||||
if (releaseSigningAvailable) {
|
||||
create("release") {
|
||||
storeFile = file(props["RELEASE_STORE_FILE"] as String)
|
||||
storePassword = props["RELEASE_STORE_PASSWORD"] as String
|
||||
keyAlias = props["RELEASE_KEY_ALIAS"] as String
|
||||
keyPassword = props["RELEASE_KEY_PASSWORD"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace = "me.kavishdevar.librepods"
|
||||
@@ -28,9 +40,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "me.kavishdevar.librepods"
|
||||
minSdk = 33
|
||||
targetSdk = 37
|
||||
versionCode = 46
|
||||
versionCode = 52
|
||||
versionName = appVersionName
|
||||
}
|
||||
buildTypes {
|
||||
@@ -46,22 +57,34 @@ android {
|
||||
}
|
||||
}
|
||||
buildConfigField("Boolean", "PLAY_BUILD", "false")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
if (releaseSigningAvailable) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
defaultConfig {
|
||||
minSdk = 33
|
||||
}
|
||||
}
|
||||
debug {
|
||||
buildConfigField("Boolean", "PLAY_BUILD", "false")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
if (releaseSigningAvailable) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
versionNameSuffix = "-debug"
|
||||
defaultConfig {
|
||||
minSdk = 33
|
||||
}
|
||||
}
|
||||
create("playRelease") {
|
||||
initWith(getByName("release"))
|
||||
}
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
dimension = "env"
|
||||
buildConfigField("Boolean", "PLAY_BUILD", "false")
|
||||
}
|
||||
create("play") {
|
||||
dimension = "env"
|
||||
buildConfigField("Boolean", "PLAY_BUILD", "true")
|
||||
versionNameSuffix = "-play"
|
||||
}
|
||||
create("playDebug") {
|
||||
initWith(getByName("debug"))
|
||||
buildConfigField("Boolean", "PLAY_BUILD", "true")
|
||||
versionNameSuffix = "-youshouldnothavethis"
|
||||
minSdk = 36
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@@ -91,25 +114,6 @@ android {
|
||||
ndkVersion = "30.0.14904198"
|
||||
|
||||
flavorDimensions += "env"
|
||||
|
||||
productFlavors {
|
||||
create("normal") {
|
||||
dimension = "env"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += "-DIS_XPOSED=OFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
create("xposed") {
|
||||
dimension = "env"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += "-DIS_XPOSED=ON"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -139,9 +143,10 @@ dependencies {
|
||||
implementation(libs.backdrop)
|
||||
// implementation(libs.hilt)
|
||||
// implementation(libs.hilt.compiler)
|
||||
add("xposedCompileOnly", libs.libxposed.api)
|
||||
add("xposedImplementation", libs.libxposed.service)
|
||||
add("playReleaseImplementation", libs.billing)
|
||||
compileOnly(libs.libxposed.api)
|
||||
implementation(libs.libxposed.service)
|
||||
implementation(libs.play.review)
|
||||
implementation(libs.play.review.ktx)
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
@@ -184,14 +189,14 @@ fun registerRootModuleZipTask(
|
||||
}
|
||||
|
||||
val zipRelease = registerRootModuleZipTask(
|
||||
"zipXposedReleaseModule",
|
||||
"xposed",
|
||||
"zipReleaseModule",
|
||||
"foss",
|
||||
"release"
|
||||
)
|
||||
|
||||
val zipDebug = registerRootModuleZipTask(
|
||||
"zipXposedDebugModule",
|
||||
"xposed",
|
||||
"zipDebugModule",
|
||||
"foss",
|
||||
"debug"
|
||||
)
|
||||
|
||||
@@ -200,22 +205,22 @@ val collect = tasks.register<Copy>("collectReleaseArtifacts") {
|
||||
dependsOn(
|
||||
zipRelease,
|
||||
zipDebug,
|
||||
"bundleXposedPlayRelease"
|
||||
"bundlePlayRelease"
|
||||
)
|
||||
|
||||
into(releaseDir)
|
||||
|
||||
from(layout.buildDirectory.dir("outputs/apk/xposed/release")) {
|
||||
from(layout.buildDirectory.dir("outputs/apk/foss/release")) {
|
||||
include("*.apk")
|
||||
rename(".*", "LibrePods-FOSS-v$appVersionName-release.apk")
|
||||
}
|
||||
|
||||
from(layout.buildDirectory.dir("outputs/apk/xposed/debug")) {
|
||||
from(layout.buildDirectory.dir("outputs/apk/foss/debug")) {
|
||||
include("*.apk")
|
||||
rename(".*", "LibrePods-FOSS-v$appVersionName-debug.apk")
|
||||
}
|
||||
|
||||
from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) {
|
||||
from(layout.buildDirectory.dir("outputs/bundle/playRelease")) {
|
||||
include("*.aab")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ cmake_minimum_required(VERSION 3.22.1)
|
||||
project("l2c_fcr_hook")
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
option(IS_XPOSED "Build Xposed components" OFF)
|
||||
|
||||
add_library(bluetooth_socket SHARED
|
||||
bluetooth_socket.cpp
|
||||
)
|
||||
@@ -24,40 +22,36 @@ target_link_libraries(bluetooth_socket
|
||||
log
|
||||
)
|
||||
|
||||
if(IS_XPOSED)
|
||||
|
||||
set(XPOSED_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../xposed/cpp)
|
||||
set(XPOSED_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../xposed/cpp)
|
||||
|
||||
add_library(l2c_fcr_hook SHARED
|
||||
${XPOSED_SRC_DIR}/l2c_fcr_hook.cpp
|
||||
add_library(l2c_fcr_hook SHARED
|
||||
l2c_fcr_hook.cpp
|
||||
|
||||
${XPOSED_SRC_DIR}/xz/xz_crc32.c
|
||||
${XPOSED_SRC_DIR}/xz/xz_crc64.c
|
||||
${XPOSED_SRC_DIR}/xz/xz_sha256.c
|
||||
${XPOSED_SRC_DIR}/xz/xz_dec_stream.c
|
||||
${XPOSED_SRC_DIR}/xz/xz_dec_lzma2.c
|
||||
${XPOSED_SRC_DIR}/xz/xz_dec_bcj.c
|
||||
)
|
||||
xz/xz_crc32.c
|
||||
xz/xz_crc64.c
|
||||
xz/xz_sha256.c
|
||||
xz/xz_dec_stream.c
|
||||
xz/xz_dec_lzma2.c
|
||||
xz/xz_dec_bcj.c
|
||||
)
|
||||
|
||||
target_include_directories(l2c_fcr_hook PRIVATE
|
||||
${XPOSED_SRC_DIR}
|
||||
${XPOSED_SRC_DIR}/xz
|
||||
)
|
||||
target_include_directories(l2c_fcr_hook PRIVATE
|
||||
xz
|
||||
)
|
||||
|
||||
target_compile_definitions(l2c_fcr_hook PRIVATE
|
||||
XZ_DEC_X86
|
||||
XZ_DEC_ARM
|
||||
XZ_DEC_ARMTHUMB
|
||||
XZ_DEC_ARM64
|
||||
XZ_DEC_ANY_CHECK
|
||||
XZ_USE_CRC64
|
||||
XZ_USE_SHA256
|
||||
XZ_DEC_CONCATENATED
|
||||
)
|
||||
target_compile_definitions(l2c_fcr_hook PRIVATE
|
||||
XZ_DEC_X86
|
||||
XZ_DEC_ARM
|
||||
XZ_DEC_ARMTHUMB
|
||||
XZ_DEC_ARM64
|
||||
XZ_DEC_ANY_CHECK
|
||||
XZ_USE_CRC64
|
||||
XZ_USE_SHA256
|
||||
XZ_DEC_CONCATENATED
|
||||
)
|
||||
|
||||
target_link_libraries(l2c_fcr_hook
|
||||
android
|
||||
log
|
||||
)
|
||||
|
||||
endif()
|
||||
target_link_libraries(l2c_fcr_hook
|
||||
android
|
||||
log
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import me.kavishdevar.librepods.utils.XposedServiceHolder
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
|
||||
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {
|
||||
|
||||
override fun onCreate() {
|
||||
XposedServiceHelper.registerListener(this)
|
||||
BillingManager.provider = BillingProviderFactory.create(this)
|
||||
@@ -24,6 +24,7 @@ package me.kavishdevar.librepods
|
||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
//import dagger.hilt.android.AndroidEntryPoint
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -65,7 +66,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
@@ -87,13 +87,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -114,6 +112,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
@@ -122,14 +121,8 @@ import dev.chrisbanes.haze.rememberHazeState
|
||||
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||
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.DeviceInfoCard
|
||||
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.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.AdaptiveStrengthScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
|
||||
@@ -159,6 +152,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
lateinit var serviceConnection: ServiceConnection
|
||||
lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||
lateinit var testReviewReceiver: BroadcastReceiver
|
||||
|
||||
//@AndroidEntryPoint
|
||||
@ExperimentalMaterial3Api
|
||||
@@ -225,8 +219,6 @@ fun Main() {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val showPlayBypassVisible = remember { mutableStateOf(false) }
|
||||
val hazeState = rememberHazeState()
|
||||
val backdrop = rememberLayerBackdrop()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
@@ -243,7 +235,7 @@ fun Main() {
|
||||
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column (
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(scrollState),
|
||||
@@ -288,173 +280,25 @@ fun Main() {
|
||||
.padding(horizontal = 12.dp, vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
StyledButton(
|
||||
onClick = { showDialog.value = true },
|
||||
backdrop = rememberLayerBackdrop(),
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.enable_app_in_xposed_or_update_device),
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Light,
|
||||
color = if (isDarkTheme) Color.White else Color.Black,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
isInteractive = false,
|
||||
surfaceColor = if (isDarkTheme) Color(0xFF862424) else Color(0xFFC94646)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.bypass_compatibility_check),
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
)
|
||||
DeviceInfoCard()
|
||||
AppInfoCard()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmationDialog(
|
||||
showDialog = showDialog,
|
||||
title = stringResource(R.string.bypass_compatibility_check),
|
||||
message = stringResource(R.string.bypass_compatiblity_check_confirmation),
|
||||
confirmText = stringResource(R.string.yes),
|
||||
dismissText = stringResource(R.string.no),
|
||||
onConfirm = {
|
||||
showDialog.value = false
|
||||
if (BuildConfig.PLAY_BUILD) {
|
||||
showPlayBypassVisible.value = true
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
showDialog.value = false
|
||||
},
|
||||
backdrop = backdrop
|
||||
// hazeState = hazeState
|
||||
)
|
||||
|
||||
if (BuildConfig.PLAY_BUILD) {
|
||||
StyledBottomSheet(
|
||||
visible = showPlayBypassVisible.value,
|
||||
onDismiss = {
|
||||
showPlayBypassVisible.value = false
|
||||
showDialog.value = true
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -515,6 +359,31 @@ fun Main() {
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (BuildConfig.PLAY_BUILD) {
|
||||
val now = System.currentTimeMillis()
|
||||
val firstConn =
|
||||
sharedPreferences.getLong("first_connection_successful_time", 0L)
|
||||
|
||||
val alreadyPrompted =
|
||||
sharedPreferences.getBoolean("review_prompted", false)
|
||||
|
||||
val oneDay = 24 * 60 * 60 * 1000L
|
||||
|
||||
if (
|
||||
firstConn != 0L &&
|
||||
!alreadyPrompted &&
|
||||
(now - firstConn) > oneDay
|
||||
) {
|
||||
triggerReviewFlow(context as? Activity ?: return@LaunchedEffect)
|
||||
|
||||
sharedPreferences.edit {
|
||||
putBoolean("review_prompted", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
@@ -652,6 +521,12 @@ fun Main() {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val binder = service as AirPodsService.LocalBinder
|
||||
airPodsService.value = binder.getService()
|
||||
|
||||
if (!sharedPreferences.contains("first_connection_successful_time")) {
|
||||
sharedPreferences.edit {
|
||||
putLong("first_connection_successful_time", System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
@@ -677,6 +552,17 @@ fun Main() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerReviewFlow(activity: Activity) {
|
||||
val manager = ReviewManagerFactory.create(activity)
|
||||
val request = manager.requestReviewFlow()
|
||||
request.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful) {
|
||||
val reviewInfo = task.result
|
||||
manager.launchReviewFlow(activity, reviewInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PermissionsScreen(
|
||||
|
||||
@@ -109,7 +109,8 @@ class AACPManager {
|
||||
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),
|
||||
DYNAMIC_END_OF_CHARGE(0x3B);
|
||||
|
||||
companion object {
|
||||
fun fromByte(byte: Byte): ControlCommandIdentifiers? =
|
||||
|
||||
@@ -131,7 +131,6 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
||||
viewModel.refreshInitialData()
|
||||
}
|
||||
|
||||
isSystemInDarkTheme()
|
||||
val hazeStateS = remember { mutableStateOf(HazeState()) }
|
||||
|
||||
StyledScaffold(
|
||||
@@ -398,6 +397,16 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "spacer_dynamic_end_of_charge") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "dynamic_end_of_charge") {
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.optimized_charging),
|
||||
description = stringResource(R.string.optimized_charging_description),
|
||||
checked = state.dynamicEndOfCharge,
|
||||
onCheckedChange = viewModel::setDynamicEndOfCharge
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "accessibility") {
|
||||
NavigationButton(
|
||||
@@ -542,19 +551,22 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
StyledButton(
|
||||
onClick = {
|
||||
viewModel.reconnectFromSavedMac()
|
||||
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reconnect_to_last_device), style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
if (state.connectionSuccessful) {
|
||||
StyledButton(
|
||||
onClick = {
|
||||
viewModel.reconnectFromSavedMac()
|
||||
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reconnect_to_last_device),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package me.kavishdevar.librepods.presentation.screens
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
@@ -157,6 +158,48 @@ fun AppSettingsScreen(
|
||||
enabled = state.isPremium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.popup_animations), 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(2.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor, RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.show_bottom_sheet_popup),
|
||||
description = stringResource(R.string.show_bottom_sheet_popup_description),
|
||||
checked = state.showBottomSheetPopup,
|
||||
onCheckedChange = viewModel::setShowBottomSheetPopup,
|
||||
independent = false
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(horizontal = 12.dp)
|
||||
)
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.show_island_popup),
|
||||
description = stringResource(R.string.show_island_popup_description),
|
||||
checked = state.showIslandPopup,
|
||||
onCheckedChange = viewModel::setShowIslandPopup,
|
||||
independent = false
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.conversational_awareness), style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
@@ -500,10 +543,23 @@ fun AppSettingsScreen(
|
||||
name = stringResource(R.string.github_issues),
|
||||
navController = navController,
|
||||
onClick = {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://github.com/kavishdevar/librepods/issues".toUri()
|
||||
val appVersion = Uri.encode("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
val device = Uri.encode("${Build.MANUFACTURER} ${Build.MODEL}")
|
||||
val androidVersion = Uri.encode("${Build.ID} (${Build.DISPLAY})")
|
||||
val appSource = Uri.encode(
|
||||
when {
|
||||
BuildConfig.PLAY_BUILD -> "Play"
|
||||
else -> "GitHub"
|
||||
}
|
||||
)
|
||||
val url = "https://github.com/kavishdevar/librepods/issues/new" +
|
||||
"?template=01-bug-report-android.yml" +
|
||||
"&app-source=$appSource" +
|
||||
"&app-version=$appVersion" +
|
||||
"&device=$device" +
|
||||
"&android-version=$androidVersion"
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
context.startActivity(intent)
|
||||
},
|
||||
independent = false
|
||||
|
||||
@@ -99,9 +99,9 @@ import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.HeadTracking
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
@@ -151,9 +151,13 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
|
||||
|
||||
var lastClickTime by remember { mutableLongStateOf(0L) }
|
||||
var shouldExplode by remember { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column (
|
||||
@@ -163,7 +167,6 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
|
||||
.layerBackdrop(backdrop)
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(topPadding))
|
||||
|
||||
@@ -194,7 +197,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
|
||||
label = "Head Gestures",
|
||||
checked = state.headGesturesEnabled,
|
||||
onCheckedChange = { viewModel.setHeadGesturesEnabled(it) },
|
||||
enabled = state.isPremium,
|
||||
enabled = state.isPremium || state.headGesturesEnabled,
|
||||
description = stringResource(R.string.head_gestures_details)
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
package me.kavishdevar.librepods.presentation.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -34,13 +33,8 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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
|
||||
@@ -48,19 +42,17 @@ 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 androidx.core.content.edit
|
||||
import androidx.navigation.NavController
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.presentation.components.SelectItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSelectList
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
import kotlin.experimental.and
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@@ -82,12 +74,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
Log.d("PressAndHoldSettingsScreen", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
|
||||
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
|
||||
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val prefKey = if (name.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
||||
val longPressActionPref = sharedPreferences.getString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
||||
Log.d("PressAndHoldSettingsScreen", "Long press action preference ($prefKey): $longPressActionPref")
|
||||
var longPressAction by remember { mutableStateOf(StemAction.valueOf(longPressActionPref ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) }
|
||||
val longPressAction = if (name.lowercase() == "left") state.leftAction else state.rightAction
|
||||
val backdrop = rememberLayerBackdrop()
|
||||
StyledScaffold(
|
||||
title = name
|
||||
@@ -105,16 +92,14 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
name = stringResource(R.string.noise_control),
|
||||
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||
onClick = {
|
||||
longPressAction = StemAction.CYCLE_NOISE_CONTROL_MODES
|
||||
sharedPreferences.edit { putString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name) }
|
||||
viewModel.setLongPressAction(name, StemAction.CYCLE_NOISE_CONTROL_MODES)
|
||||
}
|
||||
),
|
||||
SelectItem(
|
||||
name = stringResource(R.string.digital_assistant),
|
||||
selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
|
||||
onClick = {
|
||||
longPressAction = StemAction.DIGITAL_ASSISTANT
|
||||
sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name) }
|
||||
viewModel.setLongPressAction(name, StemAction.DIGITAL_ASSISTANT)
|
||||
},
|
||||
enabled = state.isPremium
|
||||
)
|
||||
@@ -162,21 +147,10 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val offListeningModeValue = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||
Log.d("PressAndHoldSettingsScreen", "Allow Off state: $offListeningModeValue")
|
||||
val allowOff = offListeningModeValue == 1.toByte()
|
||||
Log.d("PressAndHoldSettingsScreen", "Allow Off option: $allowOff")
|
||||
|
||||
val initialByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]
|
||||
?.get(0)?.toInt()
|
||||
?: sharedPreferences.getInt("long_press_byte", 0b0101)
|
||||
|
||||
var currentByte by remember { mutableIntStateOf(initialByte) }
|
||||
val currentByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
|
||||
|
||||
val listeningModeItems = mutableListOf<SelectItem>()
|
||||
if (allowOff) {
|
||||
if (state.offListeningMode) {
|
||||
listeningModeItems.add(
|
||||
SelectItem(
|
||||
name = stringResource(R.string.off),
|
||||
@@ -184,21 +158,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
iconRes = R.drawable.noise_cancellation,
|
||||
selected = (currentByte and 0x01) != 0,
|
||||
onClick = {
|
||||
val bit = 0x01
|
||||
val newValue = if ((currentByte and bit) != 0) {
|
||||
val temp = currentByte and bit.inv()
|
||||
if (countEnabledModes(temp) >= 2) temp else currentByte
|
||||
} else {
|
||||
currentByte or bit
|
||||
}
|
||||
viewModel.setControlCommandByte(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||
newValue.toByte()
|
||||
)
|
||||
sharedPreferences.edit {
|
||||
putInt("long_press_byte", newValue)
|
||||
}
|
||||
currentByte = newValue
|
||||
viewModel.toggleListeningMode(0x01)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -210,21 +170,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
iconRes = R.drawable.transparency,
|
||||
selected = (currentByte and 0x04) != 0,
|
||||
onClick = {
|
||||
val bit = 0x04
|
||||
val newValue = if ((currentByte and bit) != 0) {
|
||||
val temp = currentByte and bit.inv()
|
||||
if (countEnabledModes(temp) >= 2) temp else currentByte
|
||||
} else {
|
||||
currentByte or bit
|
||||
}
|
||||
viewModel.setControlCommandByte(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||
newValue.toByte()
|
||||
)
|
||||
sharedPreferences.edit {
|
||||
putInt("long_press_byte", newValue)
|
||||
}
|
||||
currentByte = newValue
|
||||
viewModel.toggleListeningMode(0x04)
|
||||
}
|
||||
),
|
||||
SelectItem(
|
||||
@@ -233,21 +179,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
iconRes = R.drawable.adaptive,
|
||||
selected = (currentByte and 0x08) != 0,
|
||||
onClick = {
|
||||
val bit = 0x08
|
||||
val newValue = if ((currentByte and bit) != 0) {
|
||||
val temp = currentByte and bit.inv()
|
||||
if (countEnabledModes(temp) >= 2) temp else currentByte
|
||||
} else {
|
||||
currentByte or bit
|
||||
}
|
||||
viewModel.setControlCommandByte(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||
newValue.toByte()
|
||||
)
|
||||
sharedPreferences.edit {
|
||||
putInt("long_press_byte", newValue)
|
||||
}
|
||||
currentByte = newValue
|
||||
viewModel.toggleListeningMode(0x08)
|
||||
}
|
||||
),
|
||||
SelectItem(
|
||||
@@ -256,21 +188,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
iconRes = R.drawable.noise_cancellation,
|
||||
selected = (currentByte and 0x02) != 0,
|
||||
onClick = {
|
||||
val bit = 0x02
|
||||
val newValue = if ((currentByte and bit) != 0) {
|
||||
val temp = currentByte and bit.inv()
|
||||
if (countEnabledModes(temp) >= 2) temp else currentByte
|
||||
} else {
|
||||
currentByte or bit
|
||||
}
|
||||
viewModel.setControlCommandByte(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
|
||||
newValue.toByte()
|
||||
)
|
||||
sharedPreferences.edit {
|
||||
putInt("long_press_byte", newValue)
|
||||
}
|
||||
currentByte = newValue
|
||||
viewModel.toggleListeningMode(0x02)
|
||||
}
|
||||
)
|
||||
))
|
||||
@@ -290,14 +208,4 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d("PressAndHoldSettingsScreen", "Current byte: ${modesByte.toString(2)}")
|
||||
}
|
||||
|
||||
fun countEnabledModes(byteValue: Int): Int {
|
||||
var count = 0
|
||||
if ((byteValue and 0x01) != 0) count++
|
||||
if ((byteValue and 0x02) != 0) count++
|
||||
if ((byteValue and 0x04) != 0) count++
|
||||
if ((byteValue and 0x08) != 0) count++
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -89,7 +89,11 @@ data class AirPodsUiState(
|
||||
val hearingAidData: ByteArray = byteArrayOf(),
|
||||
|
||||
val isPremium: Boolean = false,
|
||||
val vendorIdHook: Boolean = false
|
||||
val vendorIdHook: Boolean = false,
|
||||
|
||||
val dynamicEndOfCharge: Boolean = false,
|
||||
|
||||
val connectionSuccessful: Boolean = false
|
||||
)
|
||||
|
||||
class AirPodsViewModel(
|
||||
@@ -268,9 +272,16 @@ class AirPodsViewModel(
|
||||
val current = state.controlStates[identifier]
|
||||
if (current?.contentEquals(value) == true) return@update state
|
||||
|
||||
state.copy(
|
||||
controlStates = state.controlStates + (identifier to value)
|
||||
)
|
||||
if (identifier == ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE) {
|
||||
state.copy(
|
||||
dynamicEndOfCharge = value[0] == 0x01.toByte(),
|
||||
controlStates = state.controlStates + (identifier to value)
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
controlStates = state.controlStates + (identifier to value)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +316,7 @@ class AirPodsViewModel(
|
||||
ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||
ControlCommandIdentifiers.OWNS_CONNECTION,
|
||||
ControlCommandIdentifiers.PPE_TOGGLE_CONFIG,
|
||||
ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE
|
||||
)
|
||||
for (identifier in identifiersList) {
|
||||
observeControl(identifier)
|
||||
@@ -342,6 +354,9 @@ class AirPodsViewModel(
|
||||
) ?: "CYCLE_NOISE_CONTROL_MODES"
|
||||
)
|
||||
val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
|
||||
val dynamicEndOfCharge = sharedPreferences.getBoolean("dynamic_end_of_charge", false)
|
||||
|
||||
val connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -351,7 +366,9 @@ class AirPodsViewModel(
|
||||
headGesturesEnabled = headGesturesEnabled,
|
||||
leftAction = leftAction,
|
||||
rightAction = rightAction,
|
||||
vendorIdHook = vendorIdHook
|
||||
vendorIdHook = vendorIdHook,
|
||||
dynamicEndOfCharge = dynamicEndOfCharge,
|
||||
connectionSuccessful = connectionSuccessful
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -371,6 +388,14 @@ class AirPodsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setDynamicEndOfCharge(enabled: Boolean) {
|
||||
service.aacpManager.sendControlCommand(ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE.value, enabled)
|
||||
sharedPreferences.edit { putBoolean("dynamic_end_of_charge", enabled) }
|
||||
_uiState.update {
|
||||
it.copy(dynamicEndOfCharge = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadControlList() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -540,6 +565,35 @@ class AirPodsViewModel(
|
||||
service.aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
|
||||
}
|
||||
|
||||
fun setLongPressAction(side: String, action: StemAction) {
|
||||
val prefKey = if (side.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
||||
sharedPreferences.edit { putString(prefKey, action.name) }
|
||||
_uiState.update {
|
||||
if (side.lowercase() == "left") it.copy(leftAction = action) else it.copy(rightAction = action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun countEnabledModes(byteValue: Int): Int {
|
||||
var count = 0
|
||||
if ((byteValue and 0x01) != 0) count++
|
||||
if ((byteValue and 0x02) != 0) count++
|
||||
if ((byteValue and 0x04) != 0) count++
|
||||
if ((byteValue and 0x08) != 0) count++
|
||||
return count
|
||||
}
|
||||
|
||||
fun toggleListeningMode(modeBit: Int) {
|
||||
val currentByte = uiState.value.controlStates[ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
|
||||
val newValue = if ((currentByte and modeBit) != 0) {
|
||||
val temp = currentByte and modeBit.inv()
|
||||
if (countEnabledModes(temp) >= 2) temp else currentByte
|
||||
} else {
|
||||
currentByte or modeBit
|
||||
}
|
||||
setControlCommandByte(ControlCommandIdentifiers.LISTENING_MODE_CONFIGS, newValue.toByte())
|
||||
sharedPreferences.edit { putInt("long_press_byte", newValue) }
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
service.disconnectAirPods()
|
||||
if (appContext.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
@@ -12,8 +12,6 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.billing.BillingManager
|
||||
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||
import me.kavishdevar.librepods.utils.NativeBridge
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class AppSettingsUiState(
|
||||
@@ -34,7 +32,9 @@ data class AppSettingsUiState(
|
||||
val cameraPackageError: String? = null,
|
||||
val vendorIdHook: Boolean = false,
|
||||
val isPremium: Boolean = false,
|
||||
val connectionSuccessful: Boolean = false
|
||||
val connectionSuccessful: Boolean = false,
|
||||
val showBottomSheetPopup: Boolean = true,
|
||||
val showIslandPopup: Boolean = true
|
||||
)
|
||||
|
||||
class AppSettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@@ -88,12 +88,11 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
|
||||
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "",
|
||||
vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false),
|
||||
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
|
||||
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false),
|
||||
showBottomSheetPopup = sharedPreferences.getBoolean("show_bottom_sheet_popup", true),
|
||||
showIslandPopup = sharedPreferences.getBoolean("show_island_popup", true)
|
||||
)
|
||||
}
|
||||
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
|
||||
NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowPhoneBatteryInWidget(enabled: Boolean) {
|
||||
@@ -178,8 +177,17 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
||||
}
|
||||
|
||||
fun setVendorIdHook(enabled: Boolean) {
|
||||
NativeBridge.setSdpHook(enabled)
|
||||
xposedRemotePref.putBoolean("vendor_id_hook", enabled)
|
||||
_uiState.update { it.copy(vendorIdHook = enabled) }
|
||||
}
|
||||
|
||||
fun setShowBottomSheetPopup(enabled: Boolean) {
|
||||
sharedPreferences.edit { putBoolean("show_bottom_sheet_popup", enabled) }
|
||||
_uiState.update { it.copy(showBottomSheetPopup = enabled) }
|
||||
}
|
||||
|
||||
fun setShowIslandPopup(enabled: Boolean) {
|
||||
sharedPreferences.edit { putBoolean("show_island_popup", enabled) }
|
||||
_uiState.update { it.copy(showIslandPopup = enabled) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
class NoiseControlWidget : AppWidgetProvider() {
|
||||
@@ -82,8 +82,14 @@ class NoiseControlWidget : AppWidgetProvider() {
|
||||
if (intent.action == "ACTION_SET_ANC_MODE") {
|
||||
val mode = intent.getIntExtra("ANC_MODE", 1)
|
||||
Log.d("NoiseControlWidget", "Setting ANC mode to $mode")
|
||||
ServiceManager.getService()!!
|
||||
.aacpManager
|
||||
val service = ServiceManager.getService()
|
||||
|
||||
if (service == null) {
|
||||
Log.w("NoiseControlWidget", "Service unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
service.aacpManager
|
||||
.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
|
||||
mode.toByte()
|
||||
|
||||
@@ -126,6 +126,7 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_
|
||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@@ -526,7 +527,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
initializeConfig()
|
||||
|
||||
ancModeReceiver = object : BroadcastReceiver() {
|
||||
externalBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == "me.kavishdevar.librepods.SET_ANC_MODE") {
|
||||
if (intent.hasExtra("mode")) {
|
||||
@@ -539,28 +540,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
} else {
|
||||
val currentMode = ancNotification.status
|
||||
val configByte = sharedPreferences.getInt("long_press_byte", 0b0111)
|
||||
val allowOffModeValue =
|
||||
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
|
||||
val allowOffMode = allowOffModeValue?.value?.takeIf { it.isNotEmpty() }
|
||||
?.get(0) == 0x01.toByte()
|
||||
|
||||
val nextMode = if (allowOffMode) {
|
||||
when (currentMode) {
|
||||
1 -> 2
|
||||
2 -> 3
|
||||
3 -> 4
|
||||
4 -> 1
|
||||
else -> 1
|
||||
}
|
||||
} else {
|
||||
when (currentMode) {
|
||||
1 -> 2
|
||||
2 -> 3
|
||||
3 -> 4
|
||||
4 -> 2
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
val allowOffMode =
|
||||
allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() || sharedPreferences.getBoolean("off_listening_mode", true)
|
||||
val nextMode = getNextMode(currentMode = currentMode, configByte = configByte, allowOffMode)
|
||||
|
||||
aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
|
||||
@@ -568,7 +553,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
)
|
||||
Log.d(
|
||||
TAG,
|
||||
"Cycling ANC mode from $currentMode to $nextMode (offListeningMode: $allowOffMode)"
|
||||
"Cycling ANC mode from $currentMode to $nextMode"
|
||||
)
|
||||
}
|
||||
} else if (intent?.action == "me.kavishdevar.librepods.CONVO_DETECT") {
|
||||
if (intent.hasExtra("enabled")) {
|
||||
val enabled = intent.getBooleanExtra("enabled", false)
|
||||
aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value,
|
||||
enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -576,10 +569,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(ancModeReceiver, ancModeFilter, RECEIVER_EXPORTED)
|
||||
registerReceiver(externalBroadcastReceiver, externalBroadcastFilter, RECEIVER_EXPORTED)
|
||||
} else {
|
||||
@Suppress("UnspecifiedRegisterReceiverFlag") registerReceiver(
|
||||
ancModeReceiver, ancModeFilter
|
||||
externalBroadcastReceiver, externalBroadcastFilter
|
||||
)
|
||||
}
|
||||
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
@@ -1116,7 +1109,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
"AirPodsParser",
|
||||
"Audio source changed mac: ${aacpManager.audioSource?.mac}, type: ${aacpManager.audioSource?.type?.name}"
|
||||
)
|
||||
if (aacpManager.audioSource?.type != AACPManager.Companion.AudioSourceType.NONE && aacpManager.audioSource?.mac != localMac) {
|
||||
if (localMac!="" && (aacpManager.audioSource?.type != AACPManager.Companion.AudioSourceType.NONE && aacpManager.audioSource?.mac != localMac)) {
|
||||
Log.d(
|
||||
"AirPodsParser",
|
||||
"Audio source is another device, better to give up aacp control"
|
||||
@@ -1272,6 +1265,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
disconnectAudio(this@AirPodsService, device)
|
||||
}
|
||||
}
|
||||
val wasNone = inEarData == listOf(false, false)
|
||||
val nowSingle = newInEarData.count { it } == 1
|
||||
|
||||
if (wasNone && nowSingle) {
|
||||
MediaController.sendPlay()
|
||||
MediaController.iPausedTheMedia = false
|
||||
return
|
||||
}
|
||||
|
||||
if (inEarData.contains(false) && newInEarData == listOf(true, true)) {
|
||||
Log.d("AirPodsParser", "User put in both AirPods from just one.")
|
||||
@@ -1644,6 +1645,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
var popupShown = false
|
||||
fun showPopup(service: Service, name: String) {
|
||||
if (!sharedPreferences.getBoolean("show_bottom_sheet_popup", true)) {
|
||||
return
|
||||
}
|
||||
if (!Settings.canDrawOverlays(service)) {
|
||||
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
|
||||
return
|
||||
@@ -1668,6 +1672,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
otherDeviceName: String? = null
|
||||
) {
|
||||
Log.d(TAG, "Showing island window")
|
||||
if (!sharedPreferences.getBoolean("show_island_popup", true)) {
|
||||
return
|
||||
}
|
||||
if (!Settings.canDrawOverlays(service)) {
|
||||
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
|
||||
return
|
||||
@@ -1970,7 +1977,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
val allowOffModeValue =
|
||||
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
|
||||
val allowOffMode =
|
||||
allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte()
|
||||
allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() || sharedPreferences.getBoolean("off_listening_mode", true)
|
||||
it.setInt(
|
||||
R.id.widget_off_button,
|
||||
"setBackgroundResource",
|
||||
@@ -2399,8 +2406,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
}
|
||||
|
||||
val ancModeFilter = IntentFilter("me.kavishdevar.librepods.SET_ANC_MODE")
|
||||
var ancModeReceiver: BroadcastReceiver? = null
|
||||
val externalBroadcastFilter = IntentFilter().apply {
|
||||
addAction("me.kavishdevar.librepods.SET_ANC_MODE")
|
||||
addAction("me.kavishdevar.librepods.CONVO_DETECT")
|
||||
}
|
||||
var externalBroadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -2703,6 +2713,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
)
|
||||
Log.d(TAG, "<LogCollector:Complete:Success> Socket connected")
|
||||
sharedPreferences.edit { putBoolean("connection_successful", true) }
|
||||
if (!sharedPreferences.contains("first_connection_successful_time")) {
|
||||
sharedPreferences.edit {
|
||||
putLong(
|
||||
"first_connection_successful_time",
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_L2CAP_CONNECTED))
|
||||
} catch (e: Exception) {
|
||||
// sharedPreferences.edit { putBoolean("connection_successful", false) }
|
||||
@@ -3005,22 +3023,20 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
fun connectAudio(context: Context, device: BluetoothDevice?) {
|
||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
if (profile == BluetoothProfile.A2DP) {
|
||||
|
||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
if (profile == BluetoothProfile.A2DP) {
|
||||
if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
|
||||
try {
|
||||
if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
|
||||
val policyMethod = proxy.javaClass.getMethod(
|
||||
"setConnectionPolicy",
|
||||
BluetoothDevice::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
Log.d(TAG, "calling A2DP.setConnectionPolicy for ${device?.address} to 100")
|
||||
policyMethod.invoke(proxy, device, 100)
|
||||
}
|
||||
else {
|
||||
Log.d(TAG, "not setting connection policy for A2DP, no BLUETOOTH_PRIVILEGED permission")
|
||||
}
|
||||
val policyMethod = proxy.javaClass.getMethod(
|
||||
"setConnectionPolicy",
|
||||
BluetoothDevice::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
Log.d(TAG, "calling A2DP.setConnectionPolicy for ${device?.address} to 100")
|
||||
policyMethod.invoke(proxy, device, 100)
|
||||
|
||||
val connectMethod =
|
||||
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||
connectMethod.invoke(
|
||||
@@ -3035,30 +3051,35 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
val connectMethod =
|
||||
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||
connectMethod.invoke(
|
||||
proxy, device
|
||||
)
|
||||
Log.d(TAG, "not setting connection policy for A2DP, no BLUETOOTH_PRIVILEGED permission. just called connect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {}
|
||||
}, BluetoothProfile.A2DP)
|
||||
override fun onServiceDisconnected(profile: Int) {}
|
||||
}, BluetoothProfile.A2DP)
|
||||
|
||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
|
||||
try {
|
||||
if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
val policyMethod = proxy.javaClass.getMethod(
|
||||
"setConnectionPolicy",
|
||||
BluetoothDevice::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
Log.d(
|
||||
TAG,
|
||||
"calling HEADSET.setConnectionPolicy for ${device?.address} to 100"
|
||||
)
|
||||
policyMethod.invoke(proxy, device, 100)
|
||||
} else {
|
||||
Log.d(TAG, "not setting connection policy for HEADSET, no MODIFIY_PHONE_STATE permission")
|
||||
}
|
||||
val policyMethod = proxy.javaClass.getMethod(
|
||||
"setConnectionPolicy",
|
||||
BluetoothDevice::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
Log.d(
|
||||
TAG,
|
||||
"calling HEADSET.setConnectionPolicy for ${device?.address} to 100"
|
||||
)
|
||||
policyMethod.invoke(proxy, device, 100)
|
||||
val connectMethod =
|
||||
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||
connectMethod.invoke(proxy, device)
|
||||
@@ -3067,11 +3088,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
} finally {
|
||||
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "not setting connection policy for HEADSET, no MODIFIY_PHONE_STATE permission")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {}
|
||||
}, BluetoothProfile.HEADSET)
|
||||
override fun onServiceDisconnected(profile: Int) {}
|
||||
}, BluetoothProfile.HEADSET)
|
||||
}
|
||||
|
||||
fun setName(name: String) {
|
||||
@@ -3100,7 +3124,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
e.printStackTrace()
|
||||
}
|
||||
try {
|
||||
unregisterReceiver(ancModeReceiver)
|
||||
unregisterReceiver(externalBroadcastReceiver)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -3185,3 +3209,20 @@ private fun Int.dpToPx(): Int {
|
||||
val density = Resources.getSystem().displayMetrics.density
|
||||
return (this * density).toInt()
|
||||
}
|
||||
|
||||
fun getNextMode(currentMode: Int, configByte: Int, offmodeEnabled: Boolean): Int {
|
||||
val enabledModes = buildList {
|
||||
if ((configByte and 0x01) != 0 && offmodeEnabled) add(1)
|
||||
if ((configByte and 0x04) != 0) add(3)
|
||||
if ((configByte and 0x08) != 0) add(4)
|
||||
if ((configByte and 0x02) != 0) add(2)
|
||||
}
|
||||
Log.d(TAG, "currentMode: $currentMode, config: ${configByte.toString(2)}")
|
||||
|
||||
if (enabledModes.isEmpty()) return currentMode
|
||||
|
||||
val currentIndex = enabledModes.indexOf(currentMode)
|
||||
val nextIndex = if (currentIndex == -1) 0 else (currentIndex + 1) % enabledModes.size
|
||||
|
||||
return enabledModes[nextIndex]
|
||||
}
|
||||
|
||||
@@ -171,8 +171,10 @@ object MediaController {
|
||||
}
|
||||
|
||||
if (configs != null && !iPausedTheMedia) {
|
||||
val localMac = ServiceManager.getService()?.localMac ?: return
|
||||
if (localMac == "") return
|
||||
ServiceManager.getService()?.aacpManager?.sendMediaInformataion(
|
||||
ServiceManager.getService()?.localMac ?: return,
|
||||
localMac,
|
||||
isActive
|
||||
)
|
||||
Log.d("MediaController", "User changed media state themselves; will wait for ear detection pause before auto-play")
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.os.Build
|
||||
|
||||
fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
||||
val isPixel = Build.MANUFACTURER.lowercase() == "google"
|
||||
val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
|
||||
val isOppoFamily = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo", "realme")
|
||||
val isBypassFlagActive = sharedPreferences.getBoolean("bypass_device_check.v2", false)
|
||||
|
||||
if (isBypassFlagActive) return true
|
||||
@@ -31,14 +31,14 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
|
||||
if (isPixel) {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
36 -> {
|
||||
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005"
|
||||
return Build.ID.startsWith("CP1A")
|
||||
}
|
||||
|
||||
37 -> {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if (isOppoOrOnePlus) {
|
||||
} else if (isOppoFamily) {
|
||||
return Build.VERSION.SDK_INT >= 36
|
||||
}
|
||||
return false
|
||||
|
||||
7
android/app/src/main/res/values-de/strings.xml
Normal file
7
android/app/src/main/res/values-de/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="popup_animations">Popup-Animationen</string>
|
||||
<string name="show_bottom_sheet_popup">Popup unten</string>
|
||||
<string name="show_bottom_sheet_popup_description">Zeigt das Popup im iOS-Stil unten an, wenn AirPods sich verbinden.</string>
|
||||
<string name="show_island_popup">Dynamic Island Popup</string>
|
||||
<string name="show_island_popup_description">Zeigt das Popup im Dynamic-Island-Stil oben für Verbindungs- und Übergabe-Ereignisse.</string>
|
||||
</resources>
|
||||
@@ -210,4 +210,9 @@
|
||||
<string name="listening_mode_transparency_description">Deja entrar los sonidos externos</string>
|
||||
<string name="listening_mode_adaptive_description">Ajuste dinámico del ruido externo</string>
|
||||
<string name="listening_mode_noise_cancellation_description">Bloquea los sonidos externos</string>
|
||||
<string name="popup_animations">Animaciones emergentes</string>
|
||||
<string name="show_bottom_sheet_popup">Ventana emergente inferior</string>
|
||||
<string name="show_bottom_sheet_popup_description">Muestra la ventana emergente estilo iOS en la parte inferior cuando los AirPods se conectan.</string>
|
||||
<string name="show_island_popup">Ventana emergente Dynamic Island</string>
|
||||
<string name="show_island_popup_description">Muestra la ventana emergente estilo Dynamic Island en la parte superior para eventos de conexión y traspaso.</string>
|
||||
</resources>
|
||||
|
||||
@@ -210,4 +210,9 @@
|
||||
<string name="listening_mode_transparency_description">Laisser entrer les sons extérieurs</string>
|
||||
<string name="listening_mode_adaptive_description">Ajuster dynamiquement les sons extérieurs</string>
|
||||
<string name="listening_mode_noise_cancellation_description">Bloquer les sons extérieurs</string>
|
||||
<string name="popup_animations">Animations contextuelles</string>
|
||||
<string name="show_bottom_sheet_popup">Fenêtre contextuelle en bas</string>
|
||||
<string name="show_bottom_sheet_popup_description">Afficher la fenêtre contextuelle de style iOS en bas de l\'écran lors de la connexion des AirPods.</string>
|
||||
<string name="show_island_popup">Fenêtre Dynamic Island</string>
|
||||
<string name="show_island_popup_description">Afficher la fenêtre de style Dynamic Island en haut de l\'écran pour les événements de connexion et de transfert.</string>
|
||||
</resources>
|
||||
|
||||
@@ -210,4 +210,9 @@
|
||||
<string name="listening_mode_transparency_description">Permite sons externos</string>
|
||||
<string name="listening_mode_adaptive_description">Ajusta dinamicamente o ruído externo</string>
|
||||
<string name="listening_mode_noise_cancellation_description">Bloqueia sons externos</string>
|
||||
<string name="popup_animations">Animações de pop-up</string>
|
||||
<string name="show_bottom_sheet_popup">Pop-up inferior</string>
|
||||
<string name="show_bottom_sheet_popup_description">Exibe o pop-up estilo iOS na parte inferior quando os AirPods se conectam.</string>
|
||||
<string name="show_island_popup">Pop-up Dynamic Island</string>
|
||||
<string name="show_island_popup_description">Exibe o pop-up estilo Dynamic Island no topo da tela em eventos de conexão e transferência.</string>
|
||||
</resources>
|
||||
|
||||
@@ -140,6 +140,11 @@
|
||||
<string name="widget">Widget</string>
|
||||
<string name="show_phone_battery_in_widget">Show phone battery in widget</string>
|
||||
<string name="show_phone_battery_in_widget_description">Display your phone\'s battery level in the widget alongside AirPods battery</string>
|
||||
<string name="popup_animations">Popup Animations</string>
|
||||
<string name="show_bottom_sheet_popup">Bottom sheet popup</string>
|
||||
<string name="show_bottom_sheet_popup_description">Show the iOS-style modal popup at the bottom when AirPods connect.</string>
|
||||
<string name="show_island_popup">Dynamic Island popup</string>
|
||||
<string name="show_island_popup_description">Show the Dynamic Island-style popup at the top for connection and takeover events.</string>
|
||||
<string name="conversational_awareness_volume">Conversational Awareness Volume</string>
|
||||
<string name="quick_settings_tile">Quick Settings Tile</string>
|
||||
<string name="open_dialog_for_controlling">Open dialog for controlling</string>
|
||||
@@ -247,7 +252,8 @@
|
||||
\n• Google Pixel® running 17 Beta 3 and above
|
||||
\n• OnePlus devices running OxygenOS 16 or later
|
||||
\n• Oppo devices running ColorOS 16 or later
|
||||
\n\nFor details, see the project documentation.</string>
|
||||
\n\nFor details, see the project documentation.
|
||||
</string>
|
||||
<string name="name_your_own_price">(Name your own price)</string>
|
||||
<string name="compatibility_play_dialog_confirmation">
|
||||
This device may not be supported due to platform limitations and requires an Xposed framework. Tick the checkbox below and type OK to continue.
|
||||
@@ -267,4 +273,7 @@
|
||||
<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>
|
||||
<string name="optimized_charging">Optimized Charge Limit</string>
|
||||
<string name="optimized_charging_description">AirPods can learn from your daily usage and determine when to charge to an optmized limit and when to allow or full charge. This limit adapts to your daily usage and preserves your battery lifespan over time.\nThis setting may not affect unsupported AirPods, or AirPods on an older firmware version.</string>
|
||||
<string name="enable_app_in_xposed_or_update_device">Enable LibrePods in Xposed or update your device to proceed.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package me.kavishdevar.librepods
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import me.kavishdevar.librepods.billing.BillingManager
|
||||
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
||||
|
||||
class LibrePodsApplication: Application(), DefaultLifecycleObserver {
|
||||
override fun onCreate() {
|
||||
BillingManager.provider = BillingProviderFactory.create(this)
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
super<Application>.onCreate()
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
BillingManager.provider.queryPurchases()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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) { }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package me.kavishdevar.librepods.utils
|
||||
|
||||
object NativeBridge {
|
||||
fun setSdpHook(enabled: Boolean) { }
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
[versions]
|
||||
accompanistPermissions = "0.37.3"
|
||||
agp = "9.1.0"
|
||||
kotlin = "2.3.20"
|
||||
agp = "9.1.1"
|
||||
kotlin = "2.3.21"
|
||||
coreKtx = "1.18.0"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
activityCompose = "1.13.0"
|
||||
composeBom = "2026.03.01"
|
||||
composeBom = "2026.05.00"
|
||||
annotations = "26.1.0"
|
||||
navigationCompose = "2.9.7"
|
||||
navigationCompose = "2.9.8"
|
||||
constraintlayout = "2.2.1"
|
||||
haze = "1.7.2"
|
||||
hazeMaterials = "1.7.2"
|
||||
dynamicanimation = "1.1.0"
|
||||
aboutLibraries = "14.0.1"
|
||||
aboutLibraries = "14.2.0"
|
||||
materialIconsCore = "1.7.8"
|
||||
backdrop = "2.0.0-alpha03"
|
||||
billing = "8.3.0"
|
||||
hilt = "2.59.2"
|
||||
xposed = "101.0.0"
|
||||
lifecycleProcess = "2.10.0"
|
||||
play = "2.0.2"
|
||||
|
||||
[libraries]
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||
@@ -49,6 +50,8 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r
|
||||
libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed" }
|
||||
libxposed-service = { group = "io.github.libxposed", name = "service", version.ref = "xposed" }
|
||||
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
|
||||
play-review = { group = "com.google.android.play", name="review", version.ref = "play" }
|
||||
play-review-ktx = { group = "com.google.android.play", name="review-ktx", version.ref = "play" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "v0.2.6",
|
||||
"versionCode": 46,
|
||||
"zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.2.3/LibrePods-FOSS-v0.2.3-release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/kavishdevar/librepods/main/CHANGELOG.md"
|
||||
}
|
||||
"zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.2.6/LibrePods-FOSS-v0.2.6-release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/kavishdevar/librepods/main/extras/CHANGELOG.md"
|
||||
}
|
||||
1
root-module-manual/.gitignore
vendored
1
root-module-manual/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
system/
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<permissions>
|
||||
<privapp-permissions package="me.kavishdevar.librepods">
|
||||
<permission name="android.permission.BLUETOOTH_PRIVILEGED"/>
|
||||
<permission name="android.permission.MODIFY_PHONE_STATE"/>
|
||||
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
|
||||
<permission name="android.permission.LOCAL_MAC_ADDRESS"/>
|
||||
</privapp-permissions>
|
||||
</permissions>
|
||||
Reference in New Issue
Block a user