Compare commits
37 Commits
android/st
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f547cc13c0 | ||
|
|
11fa9180e2 | ||
|
|
73e55a02d6 | ||
|
|
325ef1e953 | ||
|
|
5e30531514 | ||
|
|
75fa80c17e | ||
|
|
eb1b633aff | ||
|
|
dde5d1e808 | ||
|
|
598bd3d7d8 | ||
|
|
46071f17d7 | ||
|
|
13ab2d1feb | ||
|
|
72a7637863 | ||
|
|
24686da1f3 | ||
|
|
d9359cd81a | ||
|
|
db563fa75f | ||
|
|
fb3c8c73a4 | ||
|
|
05c0a7c88b | ||
|
|
96ee2410e8 | ||
|
|
c0d915666b | ||
|
|
91ffaaa972 | ||
|
|
48ae249405 | ||
|
|
aaf82c9738 | ||
|
|
38d6f8ceae | ||
|
|
5754dbfb16 | ||
|
|
3b20540c34 | ||
|
|
595797c703 | ||
|
|
2e782ba051 | ||
|
|
3023c706bf | ||
|
|
0d582d890b | ||
|
|
9b907fdec4 | ||
|
|
43d703423a | ||
|
|
dcb25e2e52 | ||
|
|
31397f055e | ||
|
|
070713540a | ||
|
|
6574e52195 | ||
|
|
c4633d6871 | ||
|
|
5dc7e512ae |
2
.github/workflows/ci-android.yml
vendored
@@ -4,8 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- 'android/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
|
||||
87
.github/workflows/ci-linux-rust.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: Linux Build & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- linux/rust
|
||||
tags:
|
||||
- 'linux-v*'
|
||||
paths:
|
||||
- 'linux-rust/**'
|
||||
- '.github/workflows/linux-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libdbus-1-dev libpulse-dev appstream just libfuse2
|
||||
|
||||
- name: Install AppImage tools
|
||||
run: |
|
||||
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O /usr/local/bin/linuxdeploy
|
||||
chmod +x /usr/local/bin/{appimagetool,linuxdeploy}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
linux-rust/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('linux-rust/Cargo.lock') }}
|
||||
|
||||
- name: Build AppImage and Binary
|
||||
working-directory: linux-rust
|
||||
run: |
|
||||
cargo build --release --verbose
|
||||
just
|
||||
mkdir -p dist
|
||||
cp target/release/librepods dist/librepods
|
||||
mv dist/LibrePods-x86_64.AppImage dist/librepods-x86_64.AppImage
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
if: "!startsWith(github.ref, 'refs/tags/linux-v')"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: librepods-x86_64.AppImage
|
||||
path: linux-rust/dist/librepods-x86_64.AppImage
|
||||
|
||||
- name: Upload binary artifact
|
||||
if: "!startsWith(github.ref, 'refs/tags/linux-v')"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: librepods
|
||||
path: linux-rust/dist/librepods
|
||||
|
||||
- name: Create tarball for Flatpak
|
||||
if: startsWith(github.ref, 'refs/tags/linux-v')
|
||||
working-directory: linux-rust
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#linux-v}"
|
||||
just tarball "${VERSION}"
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "TAR_PATH=linux-rust/dist/librepods-v${VERSION}-source.tar.gz" >> $GITHUB_ENV
|
||||
|
||||
- name: Create GitHub Release (AppImage + binary + source)
|
||||
if: startsWith(github.ref, 'refs/tags/linux-v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
files: |
|
||||
linux-rust/dist/librepods-v${{ env.VERSION }}-source.tar.gz
|
||||
linux-rust/dist/librepods-x86_64.AppImage
|
||||
linux-rust/dist/librepods
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
9
.github/workflows/ci-linux.yml
vendored
@@ -1,10 +1,9 @@
|
||||
name: Build LibrePods Linux
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - '*'
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -34,4 +33,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: librepods-linux
|
||||
path: linux/build/librepods
|
||||
path: linux/build/librepods
|
||||
@@ -1,4 +1,2 @@
|
||||
## btl2capfix v0.0.3
|
||||
- ([#34](https://github.com/kavishdevar/librepods/pull/34)) @devnoname120 Add on-device libbluetooth patcher using a Magisk/KernelSU module (arm64-only)
|
||||
|
||||
_[See more here](https://github.com/kavishdevar/librepods/releases)_
|
||||
## LibrePods root module changelog
|
||||
_[See here](https://github.com/kavishdevar/librepods/releases)_
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[](https://github.com/kavishdevar/librepods/blob/main/LICENSE)
|
||||
[](https://github.com/kavishdevar/librepods/graphs/contributors)
|
||||
|
||||
|
||||
## What is LibrePods?
|
||||
|
||||
LibrePods unlocks Apple's exclusive AirPods features on non-Apple devices. Get access to noise control modes, adaptive transparency, ear detection, battery status, and more - all the premium features you paid for but Apple locked to their ecosystem.
|
||||
@@ -61,8 +62,8 @@ For installation and detailed info, see the [Linux README](/linux/README.md).
|
||||
|-------------------|-------------------|-------------------|
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|  | | |
|
||||
|  |  |  |
|
||||
|  |  | |
|
||||
|
||||
#### Root Requirement
|
||||
|
||||
@@ -149,3 +150,5 @@ GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program over [here](/LICENSE). If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
All trademarks, logos, and brand names are the property of their respective owners. Use of them does not imply any affiliation with or endorsement by them. All AirPods images, symbols, and the SF Pro font are the property of Apple Inc.
|
||||
|
||||
@@ -90,6 +90,13 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="librepods"
|
||||
android:host="add-magic-keys" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
@@ -3,32 +3,10 @@ cmake_minimum_required(VERSION 3.22.1)
|
||||
project("l2c_fcr_hook")
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
add_library(l2c_fcr_hook SHARED
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
l2c_fcr_hook.cpp
|
||||
l2c_fcr_hook.h)
|
||||
|
||||
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
|
||||
xz
|
||||
)
|
||||
|
||||
target_compile_definitions(l2c_fcr_hook PRIVATE
|
||||
XZ_DEC_X86
|
||||
XZ_DEC_ARM
|
||||
XZ_DEC_ARMTHUMB
|
||||
XZ_DEC_ARM64
|
||||
XZ_DEC_ANY_CHECK
|
||||
XZ_USE_CRC64
|
||||
XZ_USE_SHA256
|
||||
XZ_DEC_CONCATENATED
|
||||
)
|
||||
|
||||
target_link_libraries(l2c_fcr_hook
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
android
|
||||
log)
|
||||
log)
|
||||
@@ -1,290 +1,423 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods contributors
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
#include <android/log.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <android/log.h>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <elf.h>
|
||||
|
||||
#include <sys/system_properties.h>
|
||||
#include "l2c_fcr_hook.h"
|
||||
|
||||
extern "C" {
|
||||
#include "xz.h"
|
||||
}
|
||||
|
||||
#define LOG_TAG "LibrePods"
|
||||
#define LOG_TAG "AirPodsHook"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
static HookFunType hook_func = nullptr;
|
||||
#define L2CEVT_L2CAP_CONFIG_REQ 4
|
||||
#define L2CEVT_L2CAP_CONFIG_RSP 15
|
||||
|
||||
static uint8_t (*original_l2c_fcr_chk_chan_modes)(void*) = nullptr;
|
||||
struct t_l2c_lcb;
|
||||
typedef struct _BT_HDR {
|
||||
uint16_t event;
|
||||
uint16_t len;
|
||||
uint16_t offset;
|
||||
uint16_t layer_specific;
|
||||
uint8_t data[];
|
||||
} BT_HDR;
|
||||
|
||||
typedef struct {
|
||||
uint8_t mode;
|
||||
uint8_t tx_win_sz;
|
||||
uint8_t max_transmit;
|
||||
uint16_t rtrans_tout;
|
||||
uint16_t mon_tout;
|
||||
uint16_t mps;
|
||||
} tL2CAP_FCR;
|
||||
|
||||
// Flow spec structure
|
||||
typedef struct {
|
||||
uint8_t qos_present;
|
||||
uint8_t flow_direction;
|
||||
uint8_t service_type;
|
||||
uint32_t token_rate;
|
||||
uint32_t token_bucket_size;
|
||||
uint32_t peak_bandwidth;
|
||||
uint32_t latency;
|
||||
uint32_t delay_variation;
|
||||
} FLOW_SPEC;
|
||||
|
||||
// Configuration info structure
|
||||
typedef struct {
|
||||
uint16_t result;
|
||||
uint16_t mtu_present;
|
||||
uint16_t mtu;
|
||||
uint16_t flush_to_present;
|
||||
uint16_t flush_to;
|
||||
uint16_t qos_present;
|
||||
FLOW_SPEC qos;
|
||||
uint16_t fcr_present;
|
||||
tL2CAP_FCR fcr;
|
||||
uint16_t fcs_present;
|
||||
uint16_t fcs;
|
||||
uint16_t ext_flow_spec_present;
|
||||
FLOW_SPEC ext_flow_spec;
|
||||
} tL2CAP_CFG_INFO;
|
||||
|
||||
// Basic L2CAP link control block
|
||||
typedef struct {
|
||||
bool wait_ack;
|
||||
// Other FCR fields - not needed for our specific hook
|
||||
} tL2C_FCRB;
|
||||
|
||||
// Forward declarations for needed types
|
||||
struct t_l2c_rcb;
|
||||
struct t_l2c_lcb;
|
||||
|
||||
typedef struct t_l2c_ccb {
|
||||
struct t_l2c_ccb* p_next_ccb; // Next CCB in the chain
|
||||
struct t_l2c_ccb* p_prev_ccb; // Previous CCB in the chain
|
||||
struct t_l2c_lcb* p_lcb; // Link this CCB belongs to
|
||||
struct t_l2c_rcb* p_rcb; // Registration CB for this Channel
|
||||
uint16_t local_cid; // Local CID
|
||||
uint16_t remote_cid; // Remote CID
|
||||
uint16_t p_lcb_next; // For linking CCBs to an LCB
|
||||
uint8_t ccb_priority; // Channel priority
|
||||
uint16_t tx_mps; // MPS for outgoing messages
|
||||
uint16_t max_rx_mtu; // Max MTU we will receive
|
||||
// State variables
|
||||
bool in_use; // True when channel active
|
||||
uint8_t chnl_state; // Channel state
|
||||
uint8_t local_id; // Transaction ID for local trans
|
||||
uint8_t remote_id; // Transaction ID for remote
|
||||
uint8_t timer_entry; // Timer entry
|
||||
uint8_t is_flushable; // True if flushable
|
||||
// Configuration variables
|
||||
uint16_t our_cfg_bits; // Bitmap of local config bits
|
||||
uint16_t peer_cfg_bits; // Bitmap of peer config bits
|
||||
uint16_t config_done; // Configuration bitmask
|
||||
uint16_t remote_config_rsp_result; // Remote config response result
|
||||
tL2CAP_CFG_INFO our_cfg; // Our saved configuration options
|
||||
tL2CAP_CFG_INFO peer_cfg; // Peer's saved configuration options
|
||||
// Additional control fields
|
||||
uint8_t remote_credit_count; // Credits sent to peer
|
||||
tL2C_FCRB fcrb; // FCR info
|
||||
bool ecoc; // Enhanced Credit-based mode
|
||||
} tL2C_CCB;
|
||||
|
||||
static uint8_t (*original_l2c_fcr_chk_chan_modes)(void* p_ccb) = nullptr;
|
||||
static void (*original_l2cu_process_our_cfg_req)(tL2C_CCB* p_ccb, tL2CAP_CFG_INFO* p_cfg) = nullptr;
|
||||
static void (*original_l2c_csm_config)(tL2C_CCB* p_ccb, uint8_t event, void* p_data) = nullptr;
|
||||
static void (*original_l2cu_send_peer_info_req)(tL2C_LCB* p_lcb, uint16_t info_type) = nullptr;
|
||||
|
||||
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
||||
LOGI("l2c_fcr_chk_chan_modes hooked");
|
||||
uint8_t orig = 0;
|
||||
if (original_l2c_fcr_chk_chan_modes)
|
||||
orig = original_l2c_fcr_chk_chan_modes(p_ccb);
|
||||
|
||||
LOGI("Original returned %d, forcing 1", orig);
|
||||
LOGI("l2c_fcr_chk_chan_modes hooked, returning true.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool decompressXZ(
|
||||
const uint8_t* input,
|
||||
size_t input_size,
|
||||
std::vector<uint8_t>& output) {
|
||||
|
||||
xz_crc32_init();
|
||||
#ifdef XZ_USE_CRC64
|
||||
xz_crc64_init();
|
||||
#endif
|
||||
|
||||
struct xz_dec* dec = xz_dec_init(XZ_DYNALLOC, 64U << 20);
|
||||
if (!dec) return false;
|
||||
|
||||
struct xz_buf buf{};
|
||||
buf.in = input;
|
||||
buf.in_pos = 0;
|
||||
buf.in_size = input_size;
|
||||
|
||||
output.resize(input_size * 8);
|
||||
|
||||
buf.out = output.data();
|
||||
buf.out_pos = 0;
|
||||
buf.out_size = output.size();
|
||||
|
||||
while (true) {
|
||||
enum xz_ret ret = xz_dec_run(dec, &buf);
|
||||
|
||||
if (ret == XZ_STREAM_END)
|
||||
break;
|
||||
|
||||
if (ret != XZ_OK) {
|
||||
xz_dec_end(dec);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buf.out_pos == buf.out_size) {
|
||||
size_t old = output.size();
|
||||
output.resize(old * 2);
|
||||
buf.out = output.data();
|
||||
buf.out_size = output.size();
|
||||
}
|
||||
}
|
||||
|
||||
output.resize(buf.out_pos);
|
||||
xz_dec_end(dec);
|
||||
return true;
|
||||
void fake_l2cu_process_our_cfg_req(tL2C_CCB* p_ccb, tL2CAP_CFG_INFO* p_cfg) {
|
||||
original_l2cu_process_our_cfg_req(p_ccb, p_cfg);
|
||||
p_ccb->our_cfg.fcr.mode = 0x00;
|
||||
LOGI("Set FCR mode to Basic Mode in outgoing config request");
|
||||
}
|
||||
|
||||
static bool getLibraryPath(const char* name, std::string& out) {
|
||||
FILE* fp = fopen("/proc/self/maps", "r");
|
||||
if (!fp) return false;
|
||||
void fake_l2c_csm_config(tL2C_CCB* p_ccb, uint8_t event, void* p_data) {
|
||||
// Call the original function first to handle the specific code path where the FCR mode is checked
|
||||
original_l2c_csm_config(p_ccb, event, p_data);
|
||||
|
||||
char line[1024];
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (strstr(line, name)) {
|
||||
char* path = strchr(line, '/');
|
||||
if (path) {
|
||||
out = path;
|
||||
out.erase(out.find('\n'));
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check if this happens during CONFIG_RSP event handling
|
||||
if (event == L2CEVT_L2CAP_CONFIG_RSP) {
|
||||
p_ccb->our_cfg.fcr.mode = p_ccb->peer_cfg.fcr.mode;
|
||||
LOGI("Forced compatibility in l2c_csm_config: set our_mode=%d to match peer_mode=%d",
|
||||
p_ccb->our_cfg.fcr.mode, p_ccb->peer_cfg.fcr.mode);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
|
||||
static uintptr_t getModuleBase(const char* name) {
|
||||
FILE* fp = fopen("/proc/self/maps", "r");
|
||||
if (!fp) return 0;
|
||||
|
||||
char line[1024];
|
||||
uintptr_t base = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (strstr(line, name)) {
|
||||
base = strtoull(line, nullptr, 16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return base;
|
||||
// Replacement function that does nothing
|
||||
void fake_l2cu_send_peer_info_req(tL2C_LCB* p_lcb, uint16_t info_type) {
|
||||
LOGI("Intercepted l2cu_send_peer_info_req for info_type 0x%04x - doing nothing", info_type);
|
||||
// Just return without doing anything
|
||||
return;
|
||||
}
|
||||
|
||||
static uint64_t findSymbolOffset(
|
||||
const std::vector<uint8_t>& elf,
|
||||
const char* symbol_substring) {
|
||||
uintptr_t loadHookOffset([[maybe_unused]] const char* package_name) {
|
||||
const char* property_name = "persist.librepods.hook_offset";
|
||||
char value[PROP_VALUE_MAX] = {0};
|
||||
|
||||
auto* eh = reinterpret_cast<const Elf64_Ehdr*>(elf.data());
|
||||
auto* shdr = reinterpret_cast<const Elf64_Shdr*>(
|
||||
elf.data() + eh->e_shoff);
|
||||
int len = __system_property_get(property_name, value);
|
||||
if (len > 0) {
|
||||
LOGI("Read hook offset from property: %s", value);
|
||||
uintptr_t offset;
|
||||
char* endptr = nullptr;
|
||||
|
||||
const char* shstr =
|
||||
reinterpret_cast<const char*>(
|
||||
elf.data() + shdr[eh->e_shstrndx].sh_offset);
|
||||
|
||||
const Elf64_Shdr* symtab = nullptr;
|
||||
const Elf64_Shdr* strtab = nullptr;
|
||||
|
||||
for (int i = 0; i < eh->e_shnum; ++i) {
|
||||
const char* secname = shstr + shdr[i].sh_name;
|
||||
if (!strcmp(secname, ".symtab"))
|
||||
symtab = &shdr[i];
|
||||
if (!strcmp(secname, ".strtab"))
|
||||
strtab = &shdr[i];
|
||||
}
|
||||
|
||||
if (!symtab || !strtab)
|
||||
return 0;
|
||||
|
||||
auto* symbols = reinterpret_cast<const Elf64_Sym*>(
|
||||
elf.data() + symtab->sh_offset);
|
||||
|
||||
const char* strings =
|
||||
reinterpret_cast<const char*>(
|
||||
elf.data() + strtab->sh_offset);
|
||||
|
||||
size_t count = symtab->sh_size / sizeof(Elf64_Sym);
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const char* name = strings + symbols[i].st_name;
|
||||
|
||||
if (strstr(name, symbol_substring) &&
|
||||
ELF64_ST_TYPE(symbols[i].st_info) == STT_FUNC) {
|
||||
|
||||
LOGI("Resolved %s at 0x%lx",
|
||||
name,
|
||||
(unsigned long)symbols[i].st_value);
|
||||
|
||||
return symbols[i].st_value;
|
||||
const char* parse_start = value;
|
||||
if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
|
||||
parse_start = value + 2;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
offset = strtoul(parse_start, &endptr, 16);
|
||||
|
||||
if (errno == 0 && endptr != parse_start && *endptr == '\0' && offset > 0) {
|
||||
LOGI("Parsed offset: 0x%x", offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
LOGE("Failed to parse offset from property value: %s", value);
|
||||
}
|
||||
|
||||
LOGI("Using hardcoded fallback offset");
|
||||
return 0x00a55e30;
|
||||
}
|
||||
|
||||
uintptr_t loadL2cuProcessCfgReqOffset() {
|
||||
const char* property_name = "persist.librepods.cfg_req_offset";
|
||||
char value[PROP_VALUE_MAX] = {0};
|
||||
|
||||
int len = __system_property_get(property_name, value);
|
||||
if (len > 0) {
|
||||
LOGI("Read l2cu_process_our_cfg_req offset from property: %s", value);
|
||||
uintptr_t offset;
|
||||
char* endptr = nullptr;
|
||||
|
||||
const char* parse_start = value;
|
||||
if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
|
||||
parse_start = value + 2;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
offset = strtoul(parse_start, &endptr, 16);
|
||||
|
||||
if (errno == 0 && endptr != parse_start && *endptr == '\0' && offset > 0) {
|
||||
LOGI("Parsed l2cu_process_our_cfg_req offset: 0x%x", offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
LOGE("Failed to parse l2cu_process_our_cfg_req offset from property value: %s", value);
|
||||
}
|
||||
|
||||
// Return 0 if not found - we'll skip this hook
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool hookLibrary(const char* libname) {
|
||||
uintptr_t loadL2cCsmConfigOffset() {
|
||||
const char* property_name = "persist.librepods.csm_config_offset";
|
||||
char value[PROP_VALUE_MAX] = {0};
|
||||
|
||||
if (!hook_func) {
|
||||
LOGE("hook_func not initialized");
|
||||
return false;
|
||||
int len = __system_property_get(property_name, value);
|
||||
if (len > 0) {
|
||||
LOGI("Read l2c_csm_config offset from property: %s", value);
|
||||
uintptr_t offset;
|
||||
char* endptr = nullptr;
|
||||
|
||||
const char* parse_start = value;
|
||||
if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
|
||||
parse_start = value + 2;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
offset = strtoul(parse_start, &endptr, 16);
|
||||
|
||||
if (errno == 0 && endptr != parse_start && *endptr == '\0' && offset > 0) {
|
||||
LOGI("Parsed l2c_csm_config offset: 0x%x", offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
LOGE("Failed to parse l2c_csm_config offset from property value: %s", value);
|
||||
}
|
||||
|
||||
std::string path;
|
||||
if (!getLibraryPath(libname, path)) {
|
||||
LOGE("Failed to locate %s", libname);
|
||||
return false;
|
||||
// Return 0 if not found - we'll skip this hook
|
||||
return 0;
|
||||
}
|
||||
|
||||
uintptr_t loadL2cuSendPeerInfoReqOffset() {
|
||||
const char* property_name = "persist.librepods.peer_info_req_offset";
|
||||
char value[PROP_VALUE_MAX] = {0};
|
||||
|
||||
int len = __system_property_get(property_name, value);
|
||||
if (len > 0) {
|
||||
LOGI("Read l2cu_send_peer_info_req offset from property: %s", value);
|
||||
uintptr_t offset;
|
||||
char* endptr = nullptr;
|
||||
|
||||
const char* parse_start = value;
|
||||
if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
|
||||
parse_start = value + 2;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
offset = strtoul(parse_start, &endptr, 16);
|
||||
|
||||
if (errno == 0 && endptr != parse_start && *endptr == '\0' && offset > 0) {
|
||||
LOGI("Parsed l2cu_send_peer_info_req offset: 0x%x", offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
LOGE("Failed to parse l2cu_send_peer_info_req offset from property value: %s", value);
|
||||
}
|
||||
|
||||
int fd = open(path.c_str(), O_RDONLY);
|
||||
if (fd < 0) return false;
|
||||
// Return 0 if not found - we'll skip this hook
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct stat st{};
|
||||
if (fstat(fd, &st) != 0) {
|
||||
close(fd);
|
||||
return false;
|
||||
uintptr_t getModuleBase(const char *module_name) {
|
||||
FILE *fp;
|
||||
char line[1024];
|
||||
uintptr_t base_addr = 0;
|
||||
|
||||
fp = fopen("/proc/self/maps", "r");
|
||||
if (!fp) {
|
||||
LOGE("Failed to open /proc/self/maps");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> file(st.st_size);
|
||||
read(fd, file.data(), st.st_size);
|
||||
close(fd);
|
||||
|
||||
auto* eh = reinterpret_cast<Elf64_Ehdr*>(file.data());
|
||||
auto* shdr = reinterpret_cast<Elf64_Shdr*>(
|
||||
file.data() + eh->e_shoff);
|
||||
|
||||
const char* shstr =
|
||||
reinterpret_cast<const char*>(
|
||||
file.data() + shdr[eh->e_shstrndx].sh_offset);
|
||||
|
||||
for (int i = 0; i < eh->e_shnum; ++i) {
|
||||
|
||||
if (!strcmp(shstr + shdr[i].sh_name, ".gnu_debugdata")) {
|
||||
|
||||
std::vector<uint8_t> compressed(
|
||||
file.begin() + shdr[i].sh_offset,
|
||||
file.begin() + shdr[i].sh_offset + shdr[i].sh_size);
|
||||
|
||||
std::vector<uint8_t> decompressed;
|
||||
|
||||
if (!decompressXZ(
|
||||
compressed.data(),
|
||||
compressed.size(),
|
||||
decompressed))
|
||||
return false;
|
||||
|
||||
uintptr_t base = getModuleBase(libname);
|
||||
if (!base) return false;
|
||||
|
||||
uint64_t chk_offset =
|
||||
findSymbolOffset(decompressed,
|
||||
"l2c_fcr_chk_chan_modes");
|
||||
|
||||
if (chk_offset) {
|
||||
void* target =
|
||||
reinterpret_cast<void*>(base + chk_offset);
|
||||
|
||||
hook_func(target,
|
||||
(void*)fake_l2c_fcr_chk_chan_modes,
|
||||
(void**)&original_l2c_fcr_chk_chan_modes);
|
||||
|
||||
LOGI("Hooked l2c_fcr_chk_chan_modes");
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (strstr(line, module_name)) {
|
||||
char *start_addr_str = line;
|
||||
char *end_addr_str = strchr(line, '-');
|
||||
if (end_addr_str) {
|
||||
*end_addr_str = '\0';
|
||||
base_addr = strtoull(start_addr_str, nullptr, 16);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
fclose(fp);
|
||||
return base_addr;
|
||||
}
|
||||
|
||||
static void on_library_loaded(const char* name, void*) {
|
||||
bool findAndHookFunction(const char *library_name) {
|
||||
if (!hook_func) {
|
||||
LOGE("Hook function not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
uintptr_t base_addr = getModuleBase(library_name);
|
||||
if (!base_addr) {
|
||||
LOGE("Failed to get base address of %s", library_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load all offsets from system properties - no hardcoding
|
||||
uintptr_t l2c_fcr_offset = loadHookOffset(nullptr);
|
||||
uintptr_t l2cu_process_our_cfg_req_offset = loadL2cuProcessCfgReqOffset();
|
||||
uintptr_t l2c_csm_config_offset = loadL2cCsmConfigOffset();
|
||||
uintptr_t l2cu_send_peer_info_req_offset = loadL2cuSendPeerInfoReqOffset();
|
||||
|
||||
bool success = false;
|
||||
|
||||
// Hook l2c_fcr_chk_chan_modes - this is our primary hook
|
||||
if (l2c_fcr_offset > 0) {
|
||||
void* target = reinterpret_cast<void*>(base_addr + l2c_fcr_offset);
|
||||
LOGI("Hooking l2c_fcr_chk_chan_modes at offset: 0x%x, base: %p, target: %p",
|
||||
l2c_fcr_offset, (void*)base_addr, target);
|
||||
|
||||
int result = hook_func(target, (void*)fake_l2c_fcr_chk_chan_modes, (void**)&original_l2c_fcr_chk_chan_modes);
|
||||
if (result != 0) {
|
||||
LOGE("Failed to hook l2c_fcr_chk_chan_modes, error: %d", result);
|
||||
return false;
|
||||
}
|
||||
LOGI("Successfully hooked l2c_fcr_chk_chan_modes");
|
||||
success = true;
|
||||
} else {
|
||||
LOGE("No valid offset for l2c_fcr_chk_chan_modes found, cannot proceed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hook l2cu_process_our_cfg_req if offset is available
|
||||
if (l2cu_process_our_cfg_req_offset > 0) {
|
||||
void* target = reinterpret_cast<void*>(base_addr + l2cu_process_our_cfg_req_offset);
|
||||
LOGI("Hooking l2cu_process_our_cfg_req at offset: 0x%x, base: %p, target: %p",
|
||||
l2cu_process_our_cfg_req_offset, (void*)base_addr, target);
|
||||
|
||||
int result = hook_func(target, (void*)fake_l2cu_process_our_cfg_req, (void**)&original_l2cu_process_our_cfg_req);
|
||||
if (result != 0) {
|
||||
LOGE("Failed to hook l2cu_process_our_cfg_req, error: %d", result);
|
||||
// Continue even if this hook fails
|
||||
} else {
|
||||
LOGI("Successfully hooked l2cu_process_our_cfg_req");
|
||||
}
|
||||
} else {
|
||||
LOGI("Skipping l2cu_process_our_cfg_req hook as offset is not available");
|
||||
}
|
||||
|
||||
// Hook l2c_csm_config if offset is available
|
||||
if (l2c_csm_config_offset > 0) {
|
||||
void* target = reinterpret_cast<void*>(base_addr + l2c_csm_config_offset);
|
||||
LOGI("Hooking l2c_csm_config at offset: 0x%x, base: %p, target: %p",
|
||||
l2c_csm_config_offset, (void*)base_addr, target);
|
||||
|
||||
int result = hook_func(target, (void*)fake_l2c_csm_config, (void**)&original_l2c_csm_config);
|
||||
if (result != 0) {
|
||||
LOGE("Failed to hook l2c_csm_config, error: %d", result);
|
||||
// Continue even if this hook fails
|
||||
} else {
|
||||
LOGI("Successfully hooked l2c_csm_config");
|
||||
}
|
||||
} else {
|
||||
LOGI("Skipping l2c_csm_config hook as offset is not available");
|
||||
}
|
||||
|
||||
// Hook l2cu_send_peer_info_req if offset is available
|
||||
if (l2cu_send_peer_info_req_offset > 0) {
|
||||
void* target = reinterpret_cast<void*>(base_addr + l2cu_send_peer_info_req_offset);
|
||||
LOGI("Hooking l2cu_send_peer_info_req at offset: 0x%x, base: %p, target: %p",
|
||||
l2cu_send_peer_info_req_offset, (void*)base_addr, target);
|
||||
|
||||
int result = hook_func(target, (void*)fake_l2cu_send_peer_info_req, (void**)&original_l2cu_send_peer_info_req);
|
||||
if (result != 0) {
|
||||
LOGE("Failed to hook l2cu_send_peer_info_req, error: %d", result);
|
||||
// Continue even if this hook fails
|
||||
} else {
|
||||
LOGI("Successfully hooked l2cu_send_peer_info_req");
|
||||
}
|
||||
} else {
|
||||
LOGI("Skipping l2cu_send_peer_info_req hook as offset is not available");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void on_library_loaded(const char *name, [[maybe_unused]] void *handle) {
|
||||
if (strstr(name, "libbluetooth_jni.so")) {
|
||||
LOGI("Bluetooth JNI loaded");
|
||||
hookLibrary("libbluetooth_jni.so");
|
||||
}
|
||||
LOGI("Detected Bluetooth JNI library: %s", name);
|
||||
|
||||
if (strstr(name, "libbluetooth_qti.so")) {
|
||||
LOGI("Bluetooth QTI loaded");
|
||||
hookLibrary("libbluetooth_qti.so");
|
||||
bool hooked = findAndHookFunction("libbluetooth_jni.so");
|
||||
if (!hooked) {
|
||||
LOGE("Failed to hook Bluetooth JNI library function");
|
||||
}
|
||||
} else if (strstr(name, "libbluetooth_qti.so")) {
|
||||
LOGI("Detected Bluetooth QTI library: %s", name);
|
||||
|
||||
bool hooked = findAndHookFunction("libbluetooth_qti.so");
|
||||
if (!hooked) {
|
||||
LOGE("Failed to hook Bluetooth QTI library function");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
[[gnu::visibility("default")]]
|
||||
[[gnu::used]]
|
||||
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
|
||||
NativeOnModuleLoaded native_init(const NativeAPIEntries* entries) {
|
||||
LOGI("L2C FCR Hook module initialized");
|
||||
|
||||
LOGI("LibrePods initialized");
|
||||
|
||||
hook_func = (HookFunType)entries->hook_func;
|
||||
hook_func = entries->hook_func;
|
||||
|
||||
return on_library_loaded;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,28 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
typedef int (*HookFunType)(void *func, void *replace, void **backup);
|
||||
|
||||
typedef int (*UnhookFunType)(void *func);
|
||||
|
||||
typedef void (*NativeOnModuleLoaded)(const char *name, void *handle);
|
||||
|
||||
typedef struct {
|
||||
uint32_t version;
|
||||
void* hook_func;
|
||||
void* unhook_func;
|
||||
HookFunType hook_func;
|
||||
UnhookFunType unhook_func;
|
||||
} NativeAPIEntries;
|
||||
|
||||
typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries);
|
||||
[[maybe_unused]] typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries);
|
||||
|
||||
typedef struct t_l2c_ccb tL2C_CCB;
|
||||
typedef struct t_l2c_lcb tL2C_LCB;
|
||||
|
||||
uintptr_t loadHookOffset(const char* package_name);
|
||||
uintptr_t getModuleBase(const char *module_name);
|
||||
uintptr_t loadL2cuProcessCfgReqOffset();
|
||||
uintptr_t loadL2cCsmConfigOffset();
|
||||
uintptr_t loadL2cuSendPeerInfoReqOffset();
|
||||
bool findAndHookFunction(const char *library_path);
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
/* SPDX-License-Identifier: 0BSD */
|
||||
|
||||
/*
|
||||
* XZ decompressor
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <https://7-zip.org/>
|
||||
*/
|
||||
|
||||
#ifndef XZ_H
|
||||
#define XZ_H
|
||||
|
||||
#ifdef __KERNEL__
|
||||
# include <linux/stddef.h>
|
||||
# include <linux/types.h>
|
||||
#else
|
||||
# include <stddef.h>
|
||||
# include <stdint.h>
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* "#define XZ_EXTERN static" can be used to make extern functions static. */
|
||||
#ifndef XZ_EXTERN
|
||||
# define XZ_EXTERN extern
|
||||
#endif
|
||||
|
||||
/**
|
||||
* enum xz_mode - Operation mode
|
||||
*
|
||||
* @XZ_SINGLE: Single-call mode. This uses less RAM than
|
||||
* multi-call modes, because the LZMA2
|
||||
* dictionary doesn't need to be allocated as
|
||||
* part of the decoder state. All required data
|
||||
* structures are allocated at initialization,
|
||||
* so xz_dec_run() cannot return XZ_MEM_ERROR.
|
||||
* @XZ_PREALLOC: Multi-call mode with preallocated LZMA2
|
||||
* dictionary buffer. All data structures are
|
||||
* allocated at initialization, so xz_dec_run()
|
||||
* cannot return XZ_MEM_ERROR.
|
||||
* @XZ_DYNALLOC: Multi-call mode. The LZMA2 dictionary is
|
||||
* allocated once the required size has been
|
||||
* parsed from the stream headers. If the
|
||||
* allocation fails, xz_dec_run() will return
|
||||
* XZ_MEM_ERROR.
|
||||
*
|
||||
* It is possible to enable support only for a subset of the above
|
||||
* modes at compile time by defining XZ_DEC_SINGLE, XZ_DEC_PREALLOC,
|
||||
* or XZ_DEC_DYNALLOC. The xz_dec kernel module is always compiled
|
||||
* with support for all operation modes, but the preboot code may
|
||||
* be built with fewer features to minimize code size.
|
||||
*/
|
||||
enum xz_mode {
|
||||
XZ_SINGLE,
|
||||
XZ_PREALLOC,
|
||||
XZ_DYNALLOC
|
||||
};
|
||||
|
||||
/**
|
||||
* enum xz_ret - Return codes
|
||||
* @XZ_OK: Everything is OK so far. More input or more
|
||||
* output space is required to continue. This
|
||||
* return code is possible only in multi-call mode
|
||||
* (XZ_PREALLOC or XZ_DYNALLOC).
|
||||
* @XZ_STREAM_END: Operation finished successfully.
|
||||
* @XZ_UNSUPPORTED_CHECK: Integrity check type is not supported. Decoding
|
||||
* is still possible in multi-call mode by simply
|
||||
* calling xz_dec_run() again.
|
||||
* Note that this return value is used only if
|
||||
* XZ_DEC_ANY_CHECK was defined at build time,
|
||||
* which is not used in the kernel. Unsupported
|
||||
* check types return XZ_OPTIONS_ERROR if
|
||||
* XZ_DEC_ANY_CHECK was not defined at build time.
|
||||
* @XZ_MEM_ERROR: Allocating memory failed. This return code is
|
||||
* possible only if the decoder was initialized
|
||||
* with XZ_DYNALLOC. The amount of memory that was
|
||||
* tried to be allocated was no more than the
|
||||
* dict_max argument given to xz_dec_init().
|
||||
* @XZ_MEMLIMIT_ERROR: A bigger LZMA2 dictionary would be needed than
|
||||
* allowed by the dict_max argument given to
|
||||
* xz_dec_init(). This return value is possible
|
||||
* only in multi-call mode (XZ_PREALLOC or
|
||||
* XZ_DYNALLOC); the single-call mode (XZ_SINGLE)
|
||||
* ignores the dict_max argument.
|
||||
* @XZ_FORMAT_ERROR: File format was not recognized (wrong magic
|
||||
* bytes).
|
||||
* @XZ_OPTIONS_ERROR: This implementation doesn't support the requested
|
||||
* compression options. In the decoder this means
|
||||
* that the header CRC32 matches, but the header
|
||||
* itself specifies something that we don't support.
|
||||
* @XZ_DATA_ERROR: Compressed data is corrupt.
|
||||
* @XZ_BUF_ERROR: Cannot make any progress. Details are slightly
|
||||
* different between multi-call and single-call
|
||||
* mode; more information below.
|
||||
*
|
||||
* In multi-call mode, XZ_BUF_ERROR is returned when two consecutive calls
|
||||
* to XZ code cannot consume any input and cannot produce any new output.
|
||||
* This happens when there is no new input available, or the output buffer
|
||||
* is full while at least one output byte is still pending. Assuming your
|
||||
* code is not buggy, you can get this error only when decoding a compressed
|
||||
* stream that is truncated or otherwise corrupt.
|
||||
*
|
||||
* In single-call mode, XZ_BUF_ERROR is returned only when the output buffer
|
||||
* is too small or the compressed input is corrupt in a way that makes the
|
||||
* decoder produce more output than the caller expected. When it is
|
||||
* (relatively) clear that the compressed input is truncated, XZ_DATA_ERROR
|
||||
* is used instead of XZ_BUF_ERROR.
|
||||
*/
|
||||
enum xz_ret {
|
||||
XZ_OK,
|
||||
XZ_STREAM_END,
|
||||
XZ_UNSUPPORTED_CHECK,
|
||||
XZ_MEM_ERROR,
|
||||
XZ_MEMLIMIT_ERROR,
|
||||
XZ_FORMAT_ERROR,
|
||||
XZ_OPTIONS_ERROR,
|
||||
XZ_DATA_ERROR,
|
||||
XZ_BUF_ERROR
|
||||
};
|
||||
|
||||
/**
|
||||
* struct xz_buf - Passing input and output buffers to XZ code
|
||||
* @in: Beginning of the input buffer. This may be NULL if and only
|
||||
* if in_pos is equal to in_size.
|
||||
* @in_pos: Current position in the input buffer. This must not exceed
|
||||
* in_size.
|
||||
* @in_size: Size of the input buffer
|
||||
* @out: Beginning of the output buffer. This may be NULL if and only
|
||||
* if out_pos is equal to out_size.
|
||||
* @out_pos: Current position in the output buffer. This must not exceed
|
||||
* out_size.
|
||||
* @out_size: Size of the output buffer
|
||||
*
|
||||
* Only the contents of the output buffer from out[out_pos] onward, and
|
||||
* the variables in_pos and out_pos are modified by the XZ code.
|
||||
*/
|
||||
struct xz_buf {
|
||||
const uint8_t *in;
|
||||
size_t in_pos;
|
||||
size_t in_size;
|
||||
|
||||
uint8_t *out;
|
||||
size_t out_pos;
|
||||
size_t out_size;
|
||||
};
|
||||
|
||||
/*
|
||||
* struct xz_dec - Opaque type to hold the XZ decoder state
|
||||
*/
|
||||
struct xz_dec;
|
||||
|
||||
/**
|
||||
* xz_dec_init() - Allocate and initialize a XZ decoder state
|
||||
* @mode: Operation mode
|
||||
* @dict_max: Maximum size of the LZMA2 dictionary (history buffer) for
|
||||
* multi-call decoding. This is ignored in single-call mode
|
||||
* (mode == XZ_SINGLE). LZMA2 dictionary is always 2^n bytes
|
||||
* or 2^n + 2^(n-1) bytes (the latter sizes are less common
|
||||
* in practice), so other values for dict_max don't make sense.
|
||||
* In the kernel, dictionary sizes of 64 KiB, 128 KiB, 256 KiB,
|
||||
* 512 KiB, and 1 MiB are probably the only reasonable values,
|
||||
* except for kernel and initramfs images where a bigger
|
||||
* dictionary can be fine and useful.
|
||||
*
|
||||
* Single-call mode (XZ_SINGLE): xz_dec_run() decodes the whole stream at
|
||||
* once. The caller must provide enough output space or the decoding will
|
||||
* fail. The output space is used as the dictionary buffer, which is why
|
||||
* there is no need to allocate the dictionary as part of the decoder's
|
||||
* internal state.
|
||||
*
|
||||
* Because the output buffer is used as the workspace, streams encoded using
|
||||
* a big dictionary are not a problem in single-call mode. It is enough that
|
||||
* the output buffer is big enough to hold the actual uncompressed data; it
|
||||
* can be smaller than the dictionary size stored in the stream headers.
|
||||
*
|
||||
* Multi-call mode with preallocated dictionary (XZ_PREALLOC): dict_max bytes
|
||||
* of memory is preallocated for the LZMA2 dictionary. This way there is no
|
||||
* risk that xz_dec_run() could run out of memory, since xz_dec_run() will
|
||||
* never allocate any memory. Instead, if the preallocated dictionary is too
|
||||
* small for decoding the given input stream, xz_dec_run() will return
|
||||
* XZ_MEMLIMIT_ERROR. Thus, it is important to know what kind of data will be
|
||||
* decoded to avoid allocating excessive amount of memory for the dictionary.
|
||||
*
|
||||
* Multi-call mode with dynamically allocated dictionary (XZ_DYNALLOC):
|
||||
* dict_max specifies the maximum allowed dictionary size that xz_dec_run()
|
||||
* may allocate once it has parsed the dictionary size from the stream
|
||||
* headers. This way excessive allocations can be avoided while still
|
||||
* limiting the maximum memory usage to a sane value to prevent running the
|
||||
* system out of memory when decompressing streams from untrusted sources.
|
||||
*
|
||||
* On success, xz_dec_init() returns a pointer to struct xz_dec, which is
|
||||
* ready to be used with xz_dec_run(). If memory allocation fails,
|
||||
* xz_dec_init() returns NULL.
|
||||
*/
|
||||
XZ_EXTERN struct xz_dec *xz_dec_init(enum xz_mode mode, uint32_t dict_max);
|
||||
|
||||
/**
|
||||
* xz_dec_run() - Run the XZ decoder for a single XZ stream
|
||||
* @s: Decoder state allocated using xz_dec_init()
|
||||
* @b: Input and output buffers
|
||||
*
|
||||
* The possible return values depend on build options and operation mode.
|
||||
* See enum xz_ret for details.
|
||||
*
|
||||
* Note that if an error occurs in single-call mode (return value is not
|
||||
* XZ_STREAM_END), b->in_pos and b->out_pos are not modified and the
|
||||
* contents of the output buffer from b->out[b->out_pos] onward are
|
||||
* undefined. This is true even after XZ_BUF_ERROR, because with some filter
|
||||
* chains, there may be a second pass over the output buffer, and this pass
|
||||
* cannot be properly done if the output buffer is truncated. Thus, you
|
||||
* cannot give the single-call decoder a too small buffer and then expect to
|
||||
* get that amount valid data from the beginning of the stream. You must use
|
||||
* the multi-call decoder if you don't want to uncompress the whole stream.
|
||||
*
|
||||
* Use xz_dec_run() when XZ data is stored inside some other file format.
|
||||
* The decoding will stop after one XZ stream has been decompressed. To
|
||||
* decompress regular .xz files which might have multiple concatenated
|
||||
* streams, use xz_dec_catrun() instead.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_run(struct xz_dec *s, struct xz_buf *b);
|
||||
|
||||
/**
|
||||
* xz_dec_catrun() - Run the XZ decoder with support for concatenated streams
|
||||
* @s: Decoder state allocated using xz_dec_init()
|
||||
* @b: Input and output buffers
|
||||
* @finish: This is an int instead of bool to avoid requiring stdbool.h.
|
||||
* As long as more input might be coming, finish must be false.
|
||||
* When the caller knows that it has provided all the input to
|
||||
* the decoder (some possibly still in b->in), it must set finish
|
||||
* to true. Only when finish is true can this function return
|
||||
* XZ_STREAM_END to indicate successful decompression of the
|
||||
* file. In single-call mode (XZ_SINGLE) finish is assumed to
|
||||
* always be true; the caller-provided value is ignored.
|
||||
*
|
||||
* This is like xz_dec_run() except that this makes it easy to decode .xz
|
||||
* files with multiple streams (multiple .xz files concatenated as is).
|
||||
* The rarely-used Stream Padding feature is supported too, that is, there
|
||||
* can be null bytes after or between the streams. The number of null bytes
|
||||
* must be a multiple of four.
|
||||
*
|
||||
* When finish is false and b->in_pos == b->in_size, it is possible that
|
||||
* XZ_BUF_ERROR isn't returned even when no progress is possible (XZ_OK is
|
||||
* returned instead). This shouldn't matter because in this situation a
|
||||
* reasonable caller will attempt to provide more input or set finish to
|
||||
* true for the next xz_dec_catrun() call anyway.
|
||||
*
|
||||
* For any struct xz_dec that has been initialized for multi-call mode:
|
||||
* Once decoding has been started with xz_dec_run() or xz_dec_catrun(),
|
||||
* the same function must be used until xz_dec_reset() or xz_dec_end().
|
||||
* Switching between the two decoding functions without resetting results
|
||||
* in undefined behavior.
|
||||
*
|
||||
* xz_dec_catrun() is only available if XZ_DEC_CONCATENATED was defined
|
||||
* at compile time.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_catrun(struct xz_dec *s, struct xz_buf *b,
|
||||
int finish);
|
||||
|
||||
/**
|
||||
* xz_dec_reset() - Reset an already allocated decoder state
|
||||
* @s: Decoder state allocated using xz_dec_init()
|
||||
*
|
||||
* This function can be used to reset the multi-call decoder state without
|
||||
* freeing and reallocating memory with xz_dec_end() and xz_dec_init().
|
||||
*
|
||||
* In single-call mode, xz_dec_reset() is always called in the beginning of
|
||||
* xz_dec_run(). Thus, explicit call to xz_dec_reset() is useful only in
|
||||
* multi-call mode.
|
||||
*/
|
||||
XZ_EXTERN void xz_dec_reset(struct xz_dec *s);
|
||||
|
||||
/**
|
||||
* xz_dec_end() - Free the memory allocated for the decoder state
|
||||
* @s: Decoder state allocated using xz_dec_init(). If s is NULL,
|
||||
* this function does nothing.
|
||||
*/
|
||||
XZ_EXTERN void xz_dec_end(struct xz_dec *s);
|
||||
|
||||
/**
|
||||
* DOC: MicroLZMA decompressor
|
||||
*
|
||||
* This MicroLZMA header format was created for use in EROFS but may be used
|
||||
* by others too. **In most cases one needs the XZ APIs above instead.**
|
||||
*
|
||||
* The compressed format supported by this decoder is a raw LZMA stream
|
||||
* whose first byte (always 0x00) has been replaced with bitwise-negation
|
||||
* of the LZMA properties (lc/lp/pb) byte. For example, if lc/lp/pb is
|
||||
* 3/0/2, the first byte is 0xA2. This way the first byte can never be 0x00.
|
||||
* Just like with LZMA2, lc + lp <= 4 must be true. The LZMA end-of-stream
|
||||
* marker must not be used. The unused values are reserved for future use.
|
||||
*/
|
||||
|
||||
/*
|
||||
* struct xz_dec_microlzma - Opaque type to hold the MicroLZMA decoder state
|
||||
*/
|
||||
struct xz_dec_microlzma;
|
||||
|
||||
/**
|
||||
* xz_dec_microlzma_alloc() - Allocate memory for the MicroLZMA decoder
|
||||
* @mode: XZ_SINGLE or XZ_PREALLOC
|
||||
* @dict_size: LZMA dictionary size. This must be at least 4 KiB and
|
||||
* at most 3 GiB.
|
||||
*
|
||||
* In contrast to xz_dec_init(), this function only allocates the memory
|
||||
* and remembers the dictionary size. xz_dec_microlzma_reset() must be used
|
||||
* before calling xz_dec_microlzma_run().
|
||||
*
|
||||
* The amount of allocated memory is a little less than 30 KiB with XZ_SINGLE.
|
||||
* With XZ_PREALLOC also a dictionary buffer of dict_size bytes is allocated.
|
||||
*
|
||||
* On success, xz_dec_microlzma_alloc() returns a pointer to
|
||||
* struct xz_dec_microlzma. If memory allocation fails or
|
||||
* dict_size is invalid, NULL is returned.
|
||||
*/
|
||||
XZ_EXTERN struct xz_dec_microlzma *xz_dec_microlzma_alloc(enum xz_mode mode,
|
||||
uint32_t dict_size);
|
||||
|
||||
/**
|
||||
* xz_dec_microlzma_reset() - Reset the MicroLZMA decoder state
|
||||
* @s: Decoder state allocated using xz_dec_microlzma_alloc()
|
||||
* @comp_size: Compressed size of the input stream
|
||||
* @uncomp_size: Uncompressed size of the input stream. A value smaller
|
||||
* than the real uncompressed size of the input stream can
|
||||
* be specified if uncomp_size_is_exact is set to false.
|
||||
* uncomp_size can never be set to a value larger than the
|
||||
* expected real uncompressed size because it would eventually
|
||||
* result in XZ_DATA_ERROR.
|
||||
* @uncomp_size_is_exact: This is an int instead of bool to avoid
|
||||
* requiring stdbool.h. This should normally be set to true.
|
||||
* When this is set to false, error detection is weaker.
|
||||
*/
|
||||
XZ_EXTERN void xz_dec_microlzma_reset(struct xz_dec_microlzma *s,
|
||||
uint32_t comp_size, uint32_t uncomp_size,
|
||||
int uncomp_size_is_exact);
|
||||
|
||||
/**
|
||||
* xz_dec_microlzma_run() - Run the MicroLZMA decoder
|
||||
* @s: Decoder state initialized using xz_dec_microlzma_reset()
|
||||
* @b: Input and output buffers
|
||||
*
|
||||
* This works similarly to xz_dec_run() with a few important differences.
|
||||
* Only the differences are documented here.
|
||||
*
|
||||
* The only possible return values are XZ_OK, XZ_STREAM_END, and
|
||||
* XZ_DATA_ERROR. This function cannot return XZ_BUF_ERROR: if no progress
|
||||
* is possible due to lack of input data or output space, this function will
|
||||
* keep returning XZ_OK. Thus, the calling code must be written so that it
|
||||
* will eventually provide input and output space matching (or exceeding)
|
||||
* comp_size and uncomp_size arguments given to xz_dec_microlzma_reset().
|
||||
* If the caller cannot do this (for example, if the input file is truncated
|
||||
* or otherwise corrupt), the caller must detect this error by itself to
|
||||
* avoid an infinite loop.
|
||||
*
|
||||
* If the compressed data seems to be corrupt, XZ_DATA_ERROR is returned.
|
||||
* This can happen also when incorrect dictionary, uncompressed, or
|
||||
* compressed sizes have been specified.
|
||||
*
|
||||
* With XZ_PREALLOC only: As an extra feature, b->out may be NULL to skip over
|
||||
* uncompressed data. This way the caller doesn't need to provide a temporary
|
||||
* output buffer for the bytes that will be ignored.
|
||||
*
|
||||
* With XZ_SINGLE only: In contrast to xz_dec_run(), the return value XZ_OK
|
||||
* is also possible and thus XZ_SINGLE is actually a limited multi-call mode.
|
||||
* After XZ_OK the bytes decoded so far may be read from the output buffer.
|
||||
* It is possible to continue decoding but the variables b->out and b->out_pos
|
||||
* MUST NOT be changed by the caller. Increasing the value of b->out_size is
|
||||
* allowed to make more output space available; one doesn't need to provide
|
||||
* space for the whole uncompressed data on the first call. The input buffer
|
||||
* may be changed normally like with XZ_PREALLOC. This way input data can be
|
||||
* provided from non-contiguous memory.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_microlzma_run(struct xz_dec_microlzma *s,
|
||||
struct xz_buf *b);
|
||||
|
||||
/**
|
||||
* xz_dec_microlzma_end() - Free the memory allocated for the decoder state
|
||||
* @s: Decoder state allocated using xz_dec_microlzma_alloc().
|
||||
* If s is NULL, this function does nothing.
|
||||
*/
|
||||
XZ_EXTERN void xz_dec_microlzma_end(struct xz_dec_microlzma *s);
|
||||
|
||||
/*
|
||||
* Standalone build (userspace build or in-kernel build for boot time use)
|
||||
* needs a CRC32 implementation. For normal in-kernel use, kernel's own
|
||||
* CRC32 module is used instead, and users of this module don't need to
|
||||
* care about the functions below.
|
||||
*/
|
||||
#ifndef XZ_INTERNAL_CRC32
|
||||
# ifdef __KERNEL__
|
||||
# define XZ_INTERNAL_CRC32 0
|
||||
# else
|
||||
# define XZ_INTERNAL_CRC32 1
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
* If CRC64 support has been enabled with XZ_USE_CRC64, a CRC64
|
||||
* implementation is needed too.
|
||||
*/
|
||||
#ifndef XZ_USE_CRC64
|
||||
# undef XZ_INTERNAL_CRC64
|
||||
# define XZ_INTERNAL_CRC64 0
|
||||
#endif
|
||||
#ifndef XZ_INTERNAL_CRC64
|
||||
# ifdef __KERNEL__
|
||||
# error Using CRC64 in the kernel has not been implemented.
|
||||
# else
|
||||
# define XZ_INTERNAL_CRC64 1
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#if XZ_INTERNAL_CRC32
|
||||
/*
|
||||
* This must be called before any other xz_* function to initialize
|
||||
* the CRC32 lookup table.
|
||||
*/
|
||||
XZ_EXTERN void xz_crc32_init(void);
|
||||
|
||||
/*
|
||||
* Update CRC32 value using the polynomial from IEEE-802.3. To start a new
|
||||
* calculation, the third argument must be zero. To continue the calculation,
|
||||
* the previously returned value is passed as the third argument.
|
||||
*/
|
||||
XZ_EXTERN uint32_t xz_crc32(const uint8_t *buf, size_t size, uint32_t crc);
|
||||
#endif
|
||||
|
||||
#if XZ_INTERNAL_CRC64
|
||||
/*
|
||||
* This must be called before any other xz_* function (except xz_crc32_init())
|
||||
* to initialize the CRC64 lookup table.
|
||||
*/
|
||||
XZ_EXTERN void xz_crc64_init(void);
|
||||
|
||||
/*
|
||||
* Update CRC64 value using the polynomial from ECMA-182. To start a new
|
||||
* calculation, the third argument must be zero. To continue the calculation,
|
||||
* the previously returned value is passed as the third argument.
|
||||
*/
|
||||
XZ_EXTERN uint64_t xz_crc64(const uint8_t *buf, size_t size, uint64_t crc);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,138 +0,0 @@
|
||||
/* SPDX-License-Identifier: 0BSD */
|
||||
|
||||
/*
|
||||
* Private includes and definitions for userspace use of XZ Embedded
|
||||
*
|
||||
* Author: Lasse Collin <lasse.collin@tukaani.org>
|
||||
*/
|
||||
|
||||
#ifndef XZ_CONFIG_H
|
||||
#define XZ_CONFIG_H
|
||||
|
||||
/* Uncomment to enable building of xz_dec_catrun(). */
|
||||
/* #define XZ_DEC_CONCATENATED */
|
||||
|
||||
/* Uncomment to enable CRC64 support. */
|
||||
/* #define XZ_USE_CRC64 */
|
||||
|
||||
/* Uncomment as needed to enable BCJ filter decoders. */
|
||||
/* #define XZ_DEC_X86 */
|
||||
/* #define XZ_DEC_ARM */
|
||||
/* #define XZ_DEC_ARMTHUMB */
|
||||
/* #define XZ_DEC_ARM64 */
|
||||
/* #define XZ_DEC_RISCV */
|
||||
/* #define XZ_DEC_POWERPC */
|
||||
/* #define XZ_DEC_IA64 */
|
||||
/* #define XZ_DEC_SPARC */
|
||||
|
||||
/*
|
||||
* Visual Studio 2013 update 2 supports only __inline, not inline.
|
||||
* MSVC v19.0 / VS 2015 and newer support both.
|
||||
*/
|
||||
#if defined(_MSC_VER) && _MSC_VER < 1900 && !defined(inline)
|
||||
# define inline __inline
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "xz.h"
|
||||
|
||||
#define kmalloc(size, flags) malloc(size)
|
||||
#define kfree(ptr) free(ptr)
|
||||
#define vmalloc(size) malloc(size)
|
||||
#define vfree(ptr) free(ptr)
|
||||
|
||||
#define memeq(a, b, size) (memcmp(a, b, size) == 0)
|
||||
#define memzero(buf, size) memset(buf, 0, size)
|
||||
|
||||
#ifndef min
|
||||
# define min(x, y) ((x) < (y) ? (x) : (y))
|
||||
#endif
|
||||
#define min_t(type, x, y) min(x, y)
|
||||
|
||||
#ifndef fallthrough
|
||||
# if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311
|
||||
# define fallthrough [[fallthrough]]
|
||||
# elif (defined(__GNUC__) && __GNUC__ >= 7) \
|
||||
|| (defined(__clang_major__) && __clang_major__ >= 10)
|
||||
# define fallthrough __attribute__((__fallthrough__))
|
||||
# else
|
||||
# define fallthrough do {} while (0)
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Some functions have been marked with __always_inline to keep the
|
||||
* performance reasonable even when the compiler is optimizing for
|
||||
* small code size. You may be able to save a few bytes by #defining
|
||||
* __always_inline to plain inline, but don't complain if the code
|
||||
* becomes slow.
|
||||
*
|
||||
* NOTE: System headers on GNU/Linux may #define this macro already,
|
||||
* so if you want to change it, you need to #undef it first.
|
||||
*/
|
||||
#ifndef __always_inline
|
||||
# ifdef __GNUC__
|
||||
# define __always_inline \
|
||||
inline __attribute__((__always_inline__))
|
||||
# else
|
||||
# define __always_inline inline
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/* Inline functions to access unaligned unsigned 32-bit integers */
|
||||
#ifndef get_unaligned_le32
|
||||
static inline uint32_t get_unaligned_le32(const uint8_t *buf)
|
||||
{
|
||||
return (uint32_t)buf[0]
|
||||
| ((uint32_t)buf[1] << 8)
|
||||
| ((uint32_t)buf[2] << 16)
|
||||
| ((uint32_t)buf[3] << 24);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef get_unaligned_be32
|
||||
static inline uint32_t get_unaligned_be32(const uint8_t *buf)
|
||||
{
|
||||
return (uint32_t)((uint32_t)buf[0] << 24)
|
||||
| ((uint32_t)buf[1] << 16)
|
||||
| ((uint32_t)buf[2] << 8)
|
||||
| (uint32_t)buf[3];
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef put_unaligned_le32
|
||||
static inline void put_unaligned_le32(uint32_t val, uint8_t *buf)
|
||||
{
|
||||
buf[0] = (uint8_t)val;
|
||||
buf[1] = (uint8_t)(val >> 8);
|
||||
buf[2] = (uint8_t)(val >> 16);
|
||||
buf[3] = (uint8_t)(val >> 24);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef put_unaligned_be32
|
||||
static inline void put_unaligned_be32(uint32_t val, uint8_t *buf)
|
||||
{
|
||||
buf[0] = (uint8_t)(val >> 24);
|
||||
buf[1] = (uint8_t)(val >> 16);
|
||||
buf[2] = (uint8_t)(val >> 8);
|
||||
buf[3] = (uint8_t)val;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* To keep things simpler, use the generic unaligned methods also for
|
||||
* aligned access. The only place where performance could matter is
|
||||
* SHA-256 but files using SHA-256 aren't common.
|
||||
*/
|
||||
#ifndef get_le32
|
||||
# define get_le32 get_unaligned_le32
|
||||
#endif
|
||||
#ifndef get_be32
|
||||
# define get_be32 get_unaligned_be32
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,58 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
/*
|
||||
* CRC32 using the polynomial from IEEE-802.3
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <https://7-zip.org/>
|
||||
*/
|
||||
|
||||
/*
|
||||
* This is not the fastest implementation, but it is pretty compact.
|
||||
* The fastest versions of xz_crc32() on modern CPUs without hardware
|
||||
* accelerated CRC instruction are 3-5 times as fast as this version,
|
||||
* but they are bigger and use more memory for the lookup table.
|
||||
*/
|
||||
|
||||
#include "xz_private.h"
|
||||
|
||||
/*
|
||||
* STATIC_RW_DATA is used in the pre-boot environment on some architectures.
|
||||
* See <linux/decompress/mm.h> for details.
|
||||
*/
|
||||
#ifndef STATIC_RW_DATA
|
||||
# define STATIC_RW_DATA static
|
||||
#endif
|
||||
|
||||
STATIC_RW_DATA uint32_t xz_crc32_table[256];
|
||||
|
||||
XZ_EXTERN void xz_crc32_init(void)
|
||||
{
|
||||
const uint32_t poly = 0xEDB88320;
|
||||
|
||||
uint32_t i;
|
||||
uint32_t j;
|
||||
uint32_t r;
|
||||
|
||||
for (i = 0; i < 256; ++i) {
|
||||
r = i;
|
||||
for (j = 0; j < 8; ++j)
|
||||
r = (r >> 1) ^ (poly & ~((r & 1) - 1));
|
||||
|
||||
xz_crc32_table[i] = r;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
XZ_EXTERN uint32_t xz_crc32(const uint8_t *buf, size_t size, uint32_t crc)
|
||||
{
|
||||
crc = ~crc;
|
||||
|
||||
while (size != 0) {
|
||||
crc = xz_crc32_table[*buf++ ^ (crc & 0xFF)] ^ (crc >> 8);
|
||||
--size;
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
/*
|
||||
* CRC64 using the polynomial from ECMA-182
|
||||
*
|
||||
* This file is similar to xz_crc32.c. See the comments there.
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <https://7-zip.org/>
|
||||
*/
|
||||
|
||||
#include "xz_private.h"
|
||||
|
||||
#ifndef STATIC_RW_DATA
|
||||
# define STATIC_RW_DATA static
|
||||
#endif
|
||||
|
||||
STATIC_RW_DATA uint64_t xz_crc64_table[256];
|
||||
|
||||
XZ_EXTERN void xz_crc64_init(void)
|
||||
{
|
||||
/*
|
||||
* The ULL suffix is needed for -std=gnu89 compatibility
|
||||
* on 32-bit platforms.
|
||||
*/
|
||||
const uint64_t poly = 0xC96C5795D7870F42ULL;
|
||||
|
||||
uint32_t i;
|
||||
uint32_t j;
|
||||
uint64_t r;
|
||||
|
||||
for (i = 0; i < 256; ++i) {
|
||||
r = i;
|
||||
for (j = 0; j < 8; ++j)
|
||||
r = (r >> 1) ^ (poly & ~((r & 1) - 1));
|
||||
|
||||
xz_crc64_table[i] = r;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
XZ_EXTERN uint64_t xz_crc64(const uint8_t *buf, size_t size, uint64_t crc)
|
||||
{
|
||||
crc = ~crc;
|
||||
|
||||
while (size != 0) {
|
||||
crc = xz_crc64_table[*buf++ ^ (crc & 0xFF)] ^ (crc >> 8);
|
||||
--size;
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
@@ -1,738 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
/*
|
||||
* Branch/Call/Jump (BCJ) filter decoders
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <https://7-zip.org/>
|
||||
*/
|
||||
|
||||
#include "xz_private.h"
|
||||
|
||||
/*
|
||||
* The rest of the file is inside this ifdef. It makes things a little more
|
||||
* convenient when building without support for any BCJ filters.
|
||||
*/
|
||||
#ifdef XZ_DEC_BCJ
|
||||
|
||||
struct xz_dec_bcj {
|
||||
/* Type of the BCJ filter being used */
|
||||
enum {
|
||||
BCJ_X86 = 4, /* x86 or x86-64 */
|
||||
BCJ_POWERPC = 5, /* Big endian only */
|
||||
BCJ_IA64 = 6, /* Big or little endian */
|
||||
BCJ_ARM = 7, /* Little endian only */
|
||||
BCJ_ARMTHUMB = 8, /* Little endian only */
|
||||
BCJ_SPARC = 9, /* Big or little endian */
|
||||
BCJ_ARM64 = 10, /* AArch64 */
|
||||
BCJ_RISCV = 11 /* RV32GQC_Zfh, RV64GQC_Zfh */
|
||||
} type;
|
||||
|
||||
/*
|
||||
* Return value of the next filter in the chain. We need to preserve
|
||||
* this information across calls, because we must not call the next
|
||||
* filter anymore once it has returned XZ_STREAM_END.
|
||||
*/
|
||||
enum xz_ret ret;
|
||||
|
||||
/* True if we are operating in single-call mode. */
|
||||
bool single_call;
|
||||
|
||||
/*
|
||||
* Absolute position relative to the beginning of the uncompressed
|
||||
* data (in a single .xz Block). We care only about the lowest 32
|
||||
* bits so this doesn't need to be uint64_t even with big files.
|
||||
*/
|
||||
uint32_t pos;
|
||||
|
||||
/* x86 filter state */
|
||||
uint32_t x86_prev_mask;
|
||||
|
||||
/* Temporary space to hold the variables from struct xz_buf */
|
||||
uint8_t *out;
|
||||
size_t out_pos;
|
||||
size_t out_size;
|
||||
|
||||
struct {
|
||||
/* Amount of already filtered data in the beginning of buf */
|
||||
size_t filtered;
|
||||
|
||||
/* Total amount of data currently stored in buf */
|
||||
size_t size;
|
||||
|
||||
/*
|
||||
* Buffer to hold a mix of filtered and unfiltered data. This
|
||||
* needs to be big enough to hold Alignment + 2 * Look-ahead:
|
||||
*
|
||||
* Type Alignment Look-ahead
|
||||
* x86 1 4
|
||||
* PowerPC 4 0
|
||||
* IA-64 16 0
|
||||
* ARM 4 0
|
||||
* ARM-Thumb 2 2
|
||||
* SPARC 4 0
|
||||
*/
|
||||
uint8_t buf[16];
|
||||
} temp;
|
||||
};
|
||||
|
||||
#ifdef XZ_DEC_X86
|
||||
/*
|
||||
* This is used to test the most significant byte of a memory address
|
||||
* in an x86 instruction.
|
||||
*/
|
||||
static inline int bcj_x86_test_msbyte(uint8_t b)
|
||||
{
|
||||
return b == 0x00 || b == 0xFF;
|
||||
}
|
||||
|
||||
static size_t bcj_x86(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
static const bool mask_to_allowed_status[8]
|
||||
= { true, true, true, false, true, false, false, false };
|
||||
|
||||
static const uint8_t mask_to_bit_num[8] = { 0, 1, 2, 2, 3, 3, 3, 3 };
|
||||
|
||||
size_t i;
|
||||
size_t prev_pos = (size_t)-1;
|
||||
uint32_t prev_mask = s->x86_prev_mask;
|
||||
uint32_t src;
|
||||
uint32_t dest;
|
||||
uint32_t j;
|
||||
uint8_t b;
|
||||
|
||||
if (size <= 4)
|
||||
return 0;
|
||||
|
||||
size -= 4;
|
||||
for (i = 0; i < size; ++i) {
|
||||
if ((buf[i] & 0xFE) != 0xE8)
|
||||
continue;
|
||||
|
||||
prev_pos = i - prev_pos;
|
||||
if (prev_pos > 3) {
|
||||
prev_mask = 0;
|
||||
} else {
|
||||
prev_mask = (prev_mask << (prev_pos - 1)) & 7;
|
||||
if (prev_mask != 0) {
|
||||
b = buf[i + 4 - mask_to_bit_num[prev_mask]];
|
||||
if (!mask_to_allowed_status[prev_mask]
|
||||
|| bcj_x86_test_msbyte(b)) {
|
||||
prev_pos = i;
|
||||
prev_mask = (prev_mask << 1) | 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev_pos = i;
|
||||
|
||||
if (bcj_x86_test_msbyte(buf[i + 4])) {
|
||||
src = get_unaligned_le32(buf + i + 1);
|
||||
while (true) {
|
||||
dest = src - (s->pos + (uint32_t)i + 5);
|
||||
if (prev_mask == 0)
|
||||
break;
|
||||
|
||||
j = mask_to_bit_num[prev_mask] * 8;
|
||||
b = (uint8_t)(dest >> (24 - j));
|
||||
if (!bcj_x86_test_msbyte(b))
|
||||
break;
|
||||
|
||||
src = dest ^ (((uint32_t)1 << (32 - j)) - 1);
|
||||
}
|
||||
|
||||
dest &= 0x01FFFFFF;
|
||||
dest |= (uint32_t)0 - (dest & 0x01000000);
|
||||
put_unaligned_le32(dest, buf + i + 1);
|
||||
i += 4;
|
||||
} else {
|
||||
prev_mask = (prev_mask << 1) | 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_pos = i - prev_pos;
|
||||
s->x86_prev_mask = prev_pos > 3 ? 0 : prev_mask << (prev_pos - 1);
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_POWERPC
|
||||
static size_t bcj_powerpc(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t instr;
|
||||
|
||||
size &= ~(size_t)3;
|
||||
|
||||
for (i = 0; i < size; i += 4) {
|
||||
instr = get_unaligned_be32(buf + i);
|
||||
if ((instr & 0xFC000003) == 0x48000001) {
|
||||
instr &= 0x03FFFFFC;
|
||||
instr -= s->pos + (uint32_t)i;
|
||||
instr &= 0x03FFFFFC;
|
||||
instr |= 0x48000001;
|
||||
put_unaligned_be32(instr, buf + i);
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_IA64
|
||||
static size_t bcj_ia64(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
static const uint8_t branch_table[32] = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
4, 4, 6, 6, 0, 0, 7, 7,
|
||||
4, 4, 0, 0, 4, 4, 0, 0
|
||||
};
|
||||
|
||||
/*
|
||||
* The local variables take a little bit stack space, but it's less
|
||||
* than what LZMA2 decoder takes, so it doesn't make sense to reduce
|
||||
* stack usage here without doing that for the LZMA2 decoder too.
|
||||
*/
|
||||
|
||||
/* Loop counters */
|
||||
size_t i;
|
||||
size_t j;
|
||||
|
||||
/* Instruction slot (0, 1, or 2) in the 128-bit instruction word */
|
||||
uint32_t slot;
|
||||
|
||||
/* Bitwise offset of the instruction indicated by slot */
|
||||
uint32_t bit_pos;
|
||||
|
||||
/* bit_pos split into byte and bit parts */
|
||||
uint32_t byte_pos;
|
||||
uint32_t bit_res;
|
||||
|
||||
/* Address part of an instruction */
|
||||
uint32_t addr;
|
||||
|
||||
/* Mask used to detect which instructions to convert */
|
||||
uint32_t mask;
|
||||
|
||||
/* 41-bit instruction stored somewhere in the lowest 48 bits */
|
||||
uint64_t instr;
|
||||
|
||||
/* Instruction normalized with bit_res for easier manipulation */
|
||||
uint64_t norm;
|
||||
|
||||
size &= ~(size_t)15;
|
||||
|
||||
for (i = 0; i < size; i += 16) {
|
||||
mask = branch_table[buf[i] & 0x1F];
|
||||
for (slot = 0, bit_pos = 5; slot < 3; ++slot, bit_pos += 41) {
|
||||
if (((mask >> slot) & 1) == 0)
|
||||
continue;
|
||||
|
||||
byte_pos = bit_pos >> 3;
|
||||
bit_res = bit_pos & 7;
|
||||
instr = 0;
|
||||
for (j = 0; j < 6; ++j)
|
||||
instr |= (uint64_t)(buf[i + j + byte_pos])
|
||||
<< (8 * j);
|
||||
|
||||
norm = instr >> bit_res;
|
||||
|
||||
if (((norm >> 37) & 0x0F) == 0x05
|
||||
&& ((norm >> 9) & 0x07) == 0) {
|
||||
addr = (norm >> 13) & 0x0FFFFF;
|
||||
addr |= ((uint32_t)(norm >> 36) & 1) << 20;
|
||||
addr <<= 4;
|
||||
addr -= s->pos + (uint32_t)i;
|
||||
addr >>= 4;
|
||||
|
||||
norm &= ~((uint64_t)0x8FFFFF << 13);
|
||||
norm |= (uint64_t)(addr & 0x0FFFFF) << 13;
|
||||
norm |= (uint64_t)(addr & 0x100000)
|
||||
<< (36 - 20);
|
||||
|
||||
instr &= (1 << bit_res) - 1;
|
||||
instr |= norm << bit_res;
|
||||
|
||||
for (j = 0; j < 6; j++)
|
||||
buf[i + j + byte_pos]
|
||||
= (uint8_t)(instr >> (8 * j));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_ARM
|
||||
static size_t bcj_arm(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t addr;
|
||||
|
||||
size &= ~(size_t)3;
|
||||
|
||||
for (i = 0; i < size; i += 4) {
|
||||
if (buf[i + 3] == 0xEB) {
|
||||
addr = (uint32_t)buf[i] | ((uint32_t)buf[i + 1] << 8)
|
||||
| ((uint32_t)buf[i + 2] << 16);
|
||||
addr <<= 2;
|
||||
addr -= s->pos + (uint32_t)i + 8;
|
||||
addr >>= 2;
|
||||
buf[i] = (uint8_t)addr;
|
||||
buf[i + 1] = (uint8_t)(addr >> 8);
|
||||
buf[i + 2] = (uint8_t)(addr >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_ARMTHUMB
|
||||
static size_t bcj_armthumb(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t addr;
|
||||
|
||||
if (size < 4)
|
||||
return 0;
|
||||
|
||||
size -= 4;
|
||||
|
||||
for (i = 0; i <= size; i += 2) {
|
||||
if ((buf[i + 1] & 0xF8) == 0xF0
|
||||
&& (buf[i + 3] & 0xF8) == 0xF8) {
|
||||
addr = (((uint32_t)buf[i + 1] & 0x07) << 19)
|
||||
| ((uint32_t)buf[i] << 11)
|
||||
| (((uint32_t)buf[i + 3] & 0x07) << 8)
|
||||
| (uint32_t)buf[i + 2];
|
||||
addr <<= 1;
|
||||
addr -= s->pos + (uint32_t)i + 4;
|
||||
addr >>= 1;
|
||||
buf[i + 1] = (uint8_t)(0xF0 | ((addr >> 19) & 0x07));
|
||||
buf[i] = (uint8_t)(addr >> 11);
|
||||
buf[i + 3] = (uint8_t)(0xF8 | ((addr >> 8) & 0x07));
|
||||
buf[i + 2] = (uint8_t)addr;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_SPARC
|
||||
static size_t bcj_sparc(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t instr;
|
||||
|
||||
size &= ~(size_t)3;
|
||||
|
||||
for (i = 0; i < size; i += 4) {
|
||||
instr = get_unaligned_be32(buf + i);
|
||||
if ((instr >> 22) == 0x100 || (instr >> 22) == 0x1FF) {
|
||||
instr <<= 2;
|
||||
instr -= s->pos + (uint32_t)i;
|
||||
instr >>= 2;
|
||||
instr = ((uint32_t)0x40000000 - (instr & 0x400000))
|
||||
| 0x40000000 | (instr & 0x3FFFFF);
|
||||
put_unaligned_be32(instr, buf + i);
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_ARM64
|
||||
static size_t bcj_arm64(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t instr;
|
||||
uint32_t addr;
|
||||
|
||||
size &= ~(size_t)3;
|
||||
|
||||
for (i = 0; i < size; i += 4) {
|
||||
instr = get_unaligned_le32(buf + i);
|
||||
|
||||
if ((instr >> 26) == 0x25) {
|
||||
/* BL instruction */
|
||||
addr = instr - ((s->pos + (uint32_t)i) >> 2);
|
||||
instr = 0x94000000 | (addr & 0x03FFFFFF);
|
||||
put_unaligned_le32(instr, buf + i);
|
||||
|
||||
} else if ((instr & 0x9F000000) == 0x90000000) {
|
||||
/* ADRP instruction */
|
||||
addr = ((instr >> 29) & 3) | ((instr >> 3) & 0x1FFFFC);
|
||||
|
||||
/* Only convert values in the range +/-512 MiB. */
|
||||
if ((addr + 0x020000) & 0x1C0000)
|
||||
continue;
|
||||
|
||||
addr -= (s->pos + (uint32_t)i) >> 12;
|
||||
|
||||
instr &= 0x9000001F;
|
||||
instr |= (addr & 3) << 29;
|
||||
instr |= (addr & 0x03FFFC) << 3;
|
||||
instr |= (0U - (addr & 0x020000)) & 0xE00000;
|
||||
|
||||
put_unaligned_le32(instr, buf + i);
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_RISCV
|
||||
static size_t bcj_riscv(struct xz_dec_bcj *s, uint8_t *buf, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t b1;
|
||||
uint32_t b2;
|
||||
uint32_t b3;
|
||||
uint32_t instr;
|
||||
uint32_t instr2;
|
||||
uint32_t instr2_rs1;
|
||||
uint32_t addr;
|
||||
|
||||
if (size < 8)
|
||||
return 0;
|
||||
|
||||
size -= 8;
|
||||
|
||||
for (i = 0; i <= size; i += 2) {
|
||||
instr = buf[i];
|
||||
|
||||
if (instr == 0xEF) {
|
||||
/* JAL */
|
||||
b1 = buf[i + 1];
|
||||
if ((b1 & 0x0D) != 0)
|
||||
continue;
|
||||
|
||||
b2 = buf[i + 2];
|
||||
b3 = buf[i + 3];
|
||||
|
||||
addr = ((b1 & 0xF0) << 13) | (b2 << 9) | (b3 << 1);
|
||||
addr -= s->pos + (uint32_t)i;
|
||||
|
||||
buf[i + 1] = (uint8_t)((b1 & 0x0F)
|
||||
| ((addr >> 8) & 0xF0));
|
||||
|
||||
buf[i + 2] = (uint8_t)(((addr >> 16) & 0x0F)
|
||||
| ((addr >> 7) & 0x10)
|
||||
| ((addr << 4) & 0xE0));
|
||||
|
||||
buf[i + 3] = (uint8_t)(((addr >> 4) & 0x7F)
|
||||
| ((addr >> 13) & 0x80));
|
||||
|
||||
i += 4 - 2;
|
||||
|
||||
} else if ((instr & 0x7F) == 0x17) {
|
||||
/* AUIPC */
|
||||
instr |= (uint32_t)buf[i + 1] << 8;
|
||||
instr |= (uint32_t)buf[i + 2] << 16;
|
||||
instr |= (uint32_t)buf[i + 3] << 24;
|
||||
|
||||
if (instr & 0xE80) {
|
||||
/* AUIPC's rd doesn't equal x0 or x2. */
|
||||
instr2 = get_unaligned_le32(buf + i + 4);
|
||||
|
||||
if (((instr << 8) ^ (instr2 - 3)) & 0xF8003) {
|
||||
i += 6 - 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
addr = (instr & 0xFFFFF000) + (instr2 >> 20);
|
||||
|
||||
instr = 0x17 | (2 << 7) | (instr2 << 12);
|
||||
instr2 = addr;
|
||||
} else {
|
||||
/* AUIPC's rd equals x0 or x2. */
|
||||
instr2_rs1 = instr >> 27;
|
||||
|
||||
if ((uint32_t)((instr - 0x3117) << 18)
|
||||
>= (instr2_rs1 & 0x1D)) {
|
||||
i += 4 - 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
addr = get_unaligned_be32(buf + i + 4);
|
||||
addr -= s->pos + (uint32_t)i;
|
||||
|
||||
instr2 = (instr >> 12) | (addr << 20);
|
||||
|
||||
instr = 0x17 | (instr2_rs1 << 7)
|
||||
| ((addr + 0x800) & 0xFFFFF000);
|
||||
}
|
||||
|
||||
put_unaligned_le32(instr, buf + i);
|
||||
put_unaligned_le32(instr2, buf + i + 4);
|
||||
|
||||
i += 8 - 2;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Apply the selected BCJ filter. Update *pos and s->pos to match the amount
|
||||
* of data that got filtered.
|
||||
*
|
||||
* NOTE: This is implemented as a switch statement to avoid using function
|
||||
* pointers, which could be problematic in the kernel boot code, which must
|
||||
* avoid pointers to static data (at least on x86).
|
||||
*/
|
||||
static void bcj_apply(struct xz_dec_bcj *s,
|
||||
uint8_t *buf, size_t *pos, size_t size)
|
||||
{
|
||||
size_t filtered;
|
||||
|
||||
buf += *pos;
|
||||
size -= *pos;
|
||||
|
||||
switch (s->type) {
|
||||
#ifdef XZ_DEC_X86
|
||||
case BCJ_X86:
|
||||
filtered = bcj_x86(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_POWERPC
|
||||
case BCJ_POWERPC:
|
||||
filtered = bcj_powerpc(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_IA64
|
||||
case BCJ_IA64:
|
||||
filtered = bcj_ia64(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_ARM
|
||||
case BCJ_ARM:
|
||||
filtered = bcj_arm(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_ARMTHUMB
|
||||
case BCJ_ARMTHUMB:
|
||||
filtered = bcj_armthumb(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_SPARC
|
||||
case BCJ_SPARC:
|
||||
filtered = bcj_sparc(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_ARM64
|
||||
case BCJ_ARM64:
|
||||
filtered = bcj_arm64(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
#ifdef XZ_DEC_RISCV
|
||||
case BCJ_RISCV:
|
||||
filtered = bcj_riscv(s, buf, size);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
/* Never reached but silence compiler warnings. */
|
||||
filtered = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
*pos += filtered;
|
||||
s->pos += filtered;
|
||||
}
|
||||
|
||||
/*
|
||||
* Flush pending filtered data from temp to the output buffer.
|
||||
* Move the remaining mixture of possibly filtered and unfiltered
|
||||
* data to the beginning of temp.
|
||||
*/
|
||||
static void bcj_flush(struct xz_dec_bcj *s, struct xz_buf *b)
|
||||
{
|
||||
size_t copy_size;
|
||||
|
||||
copy_size = min_t(size_t, s->temp.filtered, b->out_size - b->out_pos);
|
||||
memcpy(b->out + b->out_pos, s->temp.buf, copy_size);
|
||||
b->out_pos += copy_size;
|
||||
|
||||
s->temp.filtered -= copy_size;
|
||||
s->temp.size -= copy_size;
|
||||
memmove(s->temp.buf, s->temp.buf + copy_size, s->temp.size);
|
||||
}
|
||||
|
||||
/*
|
||||
* The BCJ filter functions are primitive in sense that they process the
|
||||
* data in chunks of 1-16 bytes. To hide this issue, this function does
|
||||
* some buffering.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_bcj_run(struct xz_dec_bcj *s,
|
||||
struct xz_dec_lzma2 *lzma2,
|
||||
struct xz_buf *b)
|
||||
{
|
||||
size_t out_start;
|
||||
|
||||
/*
|
||||
* Flush pending already filtered data to the output buffer. Return
|
||||
* immediately if we couldn't flush everything, or if the next
|
||||
* filter in the chain had already returned XZ_STREAM_END.
|
||||
*/
|
||||
if (s->temp.filtered > 0) {
|
||||
bcj_flush(s, b);
|
||||
if (s->temp.filtered > 0)
|
||||
return XZ_OK;
|
||||
|
||||
if (s->ret == XZ_STREAM_END)
|
||||
return XZ_STREAM_END;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we have more output space than what is currently pending in
|
||||
* temp, copy the unfiltered data from temp to the output buffer
|
||||
* and try to fill the output buffer by decoding more data from the
|
||||
* next filter in the chain. Apply the BCJ filter on the new data
|
||||
* in the output buffer. If everything cannot be filtered, copy it
|
||||
* to temp and rewind the output buffer position accordingly.
|
||||
*
|
||||
* This needs to be always run when temp.size == 0 to handle a special
|
||||
* case where the output buffer is full and the next filter has no
|
||||
* more output coming but hasn't returned XZ_STREAM_END yet.
|
||||
*/
|
||||
if (s->temp.size < b->out_size - b->out_pos || s->temp.size == 0) {
|
||||
out_start = b->out_pos;
|
||||
memcpy(b->out + b->out_pos, s->temp.buf, s->temp.size);
|
||||
b->out_pos += s->temp.size;
|
||||
|
||||
s->ret = xz_dec_lzma2_run(lzma2, b);
|
||||
if (s->ret != XZ_STREAM_END
|
||||
&& (s->ret != XZ_OK || s->single_call))
|
||||
return s->ret;
|
||||
|
||||
bcj_apply(s, b->out, &out_start, b->out_pos);
|
||||
|
||||
/*
|
||||
* As an exception, if the next filter returned XZ_STREAM_END,
|
||||
* we can do that too, since the last few bytes that remain
|
||||
* unfiltered are meant to remain unfiltered.
|
||||
*/
|
||||
if (s->ret == XZ_STREAM_END)
|
||||
return XZ_STREAM_END;
|
||||
|
||||
s->temp.size = b->out_pos - out_start;
|
||||
b->out_pos -= s->temp.size;
|
||||
memcpy(s->temp.buf, b->out + b->out_pos, s->temp.size);
|
||||
|
||||
/*
|
||||
* If there wasn't enough input to the next filter to fill
|
||||
* the output buffer with unfiltered data, there's no point
|
||||
* to try decoding more data to temp.
|
||||
*/
|
||||
if (b->out_pos + s->temp.size < b->out_size)
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* We have unfiltered data in temp. If the output buffer isn't full
|
||||
* yet, try to fill the temp buffer by decoding more data from the
|
||||
* next filter. Apply the BCJ filter on temp. Then we hopefully can
|
||||
* fill the actual output buffer by copying filtered data from temp.
|
||||
* A mix of filtered and unfiltered data may be left in temp; it will
|
||||
* be taken care on the next call to this function.
|
||||
*/
|
||||
if (b->out_pos < b->out_size) {
|
||||
/* Make b->out{,_pos,_size} temporarily point to s->temp. */
|
||||
s->out = b->out;
|
||||
s->out_pos = b->out_pos;
|
||||
s->out_size = b->out_size;
|
||||
b->out = s->temp.buf;
|
||||
b->out_pos = s->temp.size;
|
||||
b->out_size = sizeof(s->temp.buf);
|
||||
|
||||
s->ret = xz_dec_lzma2_run(lzma2, b);
|
||||
|
||||
s->temp.size = b->out_pos;
|
||||
b->out = s->out;
|
||||
b->out_pos = s->out_pos;
|
||||
b->out_size = s->out_size;
|
||||
|
||||
if (s->ret != XZ_OK && s->ret != XZ_STREAM_END)
|
||||
return s->ret;
|
||||
|
||||
bcj_apply(s, s->temp.buf, &s->temp.filtered, s->temp.size);
|
||||
|
||||
/*
|
||||
* If the next filter returned XZ_STREAM_END, we mark that
|
||||
* everything is filtered, since the last unfiltered bytes
|
||||
* of the stream are meant to be left as is.
|
||||
*/
|
||||
if (s->ret == XZ_STREAM_END)
|
||||
s->temp.filtered = s->temp.size;
|
||||
|
||||
bcj_flush(s, b);
|
||||
if (s->temp.filtered > 0)
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
return s->ret;
|
||||
}
|
||||
|
||||
XZ_EXTERN struct xz_dec_bcj *xz_dec_bcj_create(bool single_call)
|
||||
{
|
||||
struct xz_dec_bcj *s = kmalloc(sizeof(*s), GFP_KERNEL);
|
||||
if (s != NULL)
|
||||
s->single_call = single_call;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
XZ_EXTERN enum xz_ret xz_dec_bcj_reset(struct xz_dec_bcj *s, uint8_t id)
|
||||
{
|
||||
switch (id) {
|
||||
#ifdef XZ_DEC_X86
|
||||
case BCJ_X86:
|
||||
#endif
|
||||
#ifdef XZ_DEC_POWERPC
|
||||
case BCJ_POWERPC:
|
||||
#endif
|
||||
#ifdef XZ_DEC_IA64
|
||||
case BCJ_IA64:
|
||||
#endif
|
||||
#ifdef XZ_DEC_ARM
|
||||
case BCJ_ARM:
|
||||
#endif
|
||||
#ifdef XZ_DEC_ARMTHUMB
|
||||
case BCJ_ARMTHUMB:
|
||||
#endif
|
||||
#ifdef XZ_DEC_SPARC
|
||||
case BCJ_SPARC:
|
||||
#endif
|
||||
#ifdef XZ_DEC_ARM64
|
||||
case BCJ_ARM64:
|
||||
#endif
|
||||
#ifdef XZ_DEC_RISCV
|
||||
case BCJ_RISCV:
|
||||
#endif
|
||||
break;
|
||||
|
||||
default:
|
||||
/* Unsupported Filter ID */
|
||||
return XZ_OPTIONS_ERROR;
|
||||
}
|
||||
|
||||
s->type = id;
|
||||
s->ret = XZ_OK;
|
||||
s->pos = 0;
|
||||
s->x86_prev_mask = 0;
|
||||
s->temp.filtered = 0;
|
||||
s->temp.size = 0;
|
||||
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,984 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
/*
|
||||
* .xz Stream decoder
|
||||
*
|
||||
* Author: Lasse Collin <lasse.collin@tukaani.org>
|
||||
*/
|
||||
|
||||
#include "xz_private.h"
|
||||
#include "xz_stream.h"
|
||||
|
||||
#ifdef XZ_USE_CRC64
|
||||
# define IS_CRC64(check_type) ((check_type) == XZ_CHECK_CRC64)
|
||||
#else
|
||||
# define IS_CRC64(check_type) false
|
||||
#endif
|
||||
|
||||
#ifdef XZ_USE_SHA256
|
||||
# define IS_SHA256(check_type) ((check_type) == XZ_CHECK_SHA256)
|
||||
#else
|
||||
# define IS_SHA256(check_type) false
|
||||
#endif
|
||||
|
||||
/* Hash used to validate the Index field */
|
||||
struct xz_dec_hash {
|
||||
vli_type unpadded;
|
||||
vli_type uncompressed;
|
||||
uint32_t crc32;
|
||||
};
|
||||
|
||||
struct xz_dec {
|
||||
/* Position in dec_main() */
|
||||
enum {
|
||||
SEQ_STREAM_HEADER,
|
||||
SEQ_BLOCK_START,
|
||||
SEQ_BLOCK_HEADER,
|
||||
SEQ_BLOCK_UNCOMPRESS,
|
||||
SEQ_BLOCK_PADDING,
|
||||
SEQ_BLOCK_CHECK,
|
||||
SEQ_INDEX,
|
||||
SEQ_INDEX_PADDING,
|
||||
SEQ_INDEX_CRC32,
|
||||
SEQ_STREAM_FOOTER,
|
||||
SEQ_STREAM_PADDING
|
||||
} sequence;
|
||||
|
||||
/* Position in variable-length integers and Check fields */
|
||||
uint32_t pos;
|
||||
|
||||
/* Variable-length integer decoded by dec_vli() */
|
||||
vli_type vli;
|
||||
|
||||
/* Saved in_pos and out_pos */
|
||||
size_t in_start;
|
||||
size_t out_start;
|
||||
|
||||
#ifdef XZ_USE_CRC64
|
||||
/* CRC32 or CRC64 value in Block or CRC32 value in Index */
|
||||
uint64_t crc;
|
||||
#else
|
||||
/* CRC32 value in Block or Index */
|
||||
uint32_t crc;
|
||||
#endif
|
||||
|
||||
/* Type of the integrity check calculated from uncompressed data */
|
||||
enum xz_check check_type;
|
||||
|
||||
/* Operation mode */
|
||||
enum xz_mode mode;
|
||||
|
||||
/*
|
||||
* True if the next call to xz_dec_run() is allowed to return
|
||||
* XZ_BUF_ERROR.
|
||||
*/
|
||||
bool allow_buf_error;
|
||||
|
||||
/* Information stored in Block Header */
|
||||
struct {
|
||||
/*
|
||||
* Value stored in the Compressed Size field, or
|
||||
* VLI_UNKNOWN if Compressed Size is not present.
|
||||
*/
|
||||
vli_type compressed;
|
||||
|
||||
/*
|
||||
* Value stored in the Uncompressed Size field, or
|
||||
* VLI_UNKNOWN if Uncompressed Size is not present.
|
||||
*/
|
||||
vli_type uncompressed;
|
||||
|
||||
/* Size of the Block Header field */
|
||||
uint32_t size;
|
||||
} block_header;
|
||||
|
||||
/* Information collected when decoding Blocks */
|
||||
struct {
|
||||
/* Observed compressed size of the current Block */
|
||||
vli_type compressed;
|
||||
|
||||
/* Observed uncompressed size of the current Block */
|
||||
vli_type uncompressed;
|
||||
|
||||
/* Number of Blocks decoded so far */
|
||||
vli_type count;
|
||||
|
||||
/*
|
||||
* Hash calculated from the Block sizes. This is used to
|
||||
* validate the Index field.
|
||||
*/
|
||||
struct xz_dec_hash hash;
|
||||
} block;
|
||||
|
||||
/* Variables needed when verifying the Index field */
|
||||
struct {
|
||||
/* Position in dec_index() */
|
||||
enum {
|
||||
SEQ_INDEX_COUNT,
|
||||
SEQ_INDEX_UNPADDED,
|
||||
SEQ_INDEX_UNCOMPRESSED
|
||||
} sequence;
|
||||
|
||||
/* Size of the Index in bytes */
|
||||
vli_type size;
|
||||
|
||||
/* Number of Records (matches block.count in valid files) */
|
||||
vli_type count;
|
||||
|
||||
/*
|
||||
* Hash calculated from the Records (matches block.hash in
|
||||
* valid files).
|
||||
*/
|
||||
struct xz_dec_hash hash;
|
||||
} index;
|
||||
|
||||
/*
|
||||
* Temporary buffer needed to hold Stream Header, Block Header,
|
||||
* and Stream Footer. The Block Header is the biggest (1 KiB)
|
||||
* so we reserve space according to that. buf[] has to be aligned
|
||||
* to a multiple of four bytes; the size_t variables before it
|
||||
* should guarantee this.
|
||||
*/
|
||||
struct {
|
||||
size_t pos;
|
||||
size_t size;
|
||||
uint8_t buf[1024];
|
||||
} temp;
|
||||
|
||||
struct xz_dec_lzma2 *lzma2;
|
||||
|
||||
#ifdef XZ_DEC_BCJ
|
||||
struct xz_dec_bcj *bcj;
|
||||
bool bcj_active;
|
||||
#endif
|
||||
|
||||
#ifdef XZ_USE_SHA256
|
||||
/*
|
||||
* SHA-256 value in Block
|
||||
*
|
||||
* struct xz_sha256 is over a hundred bytes and it's only accessed
|
||||
* from a few places. By putting the SHA-256 state near the end
|
||||
* of struct xz_dec (somewhere after the "index" member) reduces
|
||||
* code size at least on x86 and RISC-V. It's because the first bytes
|
||||
* of the struct can be accessed with smaller instructions; the
|
||||
* members that are accessed from many places should be at the top.
|
||||
*/
|
||||
struct xz_sha256 sha256;
|
||||
#endif
|
||||
};
|
||||
|
||||
#if defined(XZ_DEC_ANY_CHECK) || defined(XZ_USE_SHA256)
|
||||
/* Sizes of the Check field with different Check IDs */
|
||||
static const uint8_t check_sizes[16] = {
|
||||
0,
|
||||
4, 4, 4,
|
||||
8, 8, 8,
|
||||
16, 16, 16,
|
||||
32, 32, 32,
|
||||
64, 64, 64
|
||||
};
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Fill s->temp by copying data starting from b->in[b->in_pos]. Caller
|
||||
* must have set s->temp.pos and s->temp.size to indicate how much data
|
||||
* we are supposed to copy into s->temp.buf. Return true once s->temp.pos
|
||||
* has reached s->temp.size.
|
||||
*/
|
||||
static bool fill_temp(struct xz_dec *s, struct xz_buf *b)
|
||||
{
|
||||
size_t copy_size = min_t(size_t,
|
||||
b->in_size - b->in_pos, s->temp.size - s->temp.pos);
|
||||
|
||||
memcpy(s->temp.buf + s->temp.pos, b->in + b->in_pos, copy_size);
|
||||
b->in_pos += copy_size;
|
||||
s->temp.pos += copy_size;
|
||||
|
||||
if (s->temp.pos == s->temp.size) {
|
||||
s->temp.pos = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Decode a variable-length integer (little-endian base-128 encoding) */
|
||||
static enum xz_ret dec_vli(struct xz_dec *s, const uint8_t *in,
|
||||
size_t *in_pos, size_t in_size)
|
||||
{
|
||||
uint8_t byte;
|
||||
|
||||
if (s->pos == 0)
|
||||
s->vli = 0;
|
||||
|
||||
while (*in_pos < in_size) {
|
||||
byte = in[*in_pos];
|
||||
++*in_pos;
|
||||
|
||||
s->vli |= (vli_type)(byte & 0x7F) << s->pos;
|
||||
|
||||
if ((byte & 0x80) == 0) {
|
||||
/* Don't allow non-minimal encodings. */
|
||||
if (byte == 0 && s->pos != 0)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->pos = 0;
|
||||
return XZ_STREAM_END;
|
||||
}
|
||||
|
||||
s->pos += 7;
|
||||
if (s->pos == 7 * VLI_BYTES_MAX)
|
||||
return XZ_DATA_ERROR;
|
||||
}
|
||||
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode the Compressed Data field from a Block. Update and validate
|
||||
* the observed compressed and uncompressed sizes of the Block so that
|
||||
* they don't exceed the values possibly stored in the Block Header
|
||||
* (validation assumes that no integer overflow occurs, since vli_type
|
||||
* is normally uint64_t). Update the CRC32 or CRC64 value if presence of
|
||||
* the CRC32 or CRC64 field was indicated in Stream Header.
|
||||
*
|
||||
* Once the decoding is finished, validate that the observed sizes match
|
||||
* the sizes possibly stored in the Block Header. Update the hash and
|
||||
* Block count, which are later used to validate the Index field.
|
||||
*/
|
||||
static enum xz_ret dec_block(struct xz_dec *s, struct xz_buf *b)
|
||||
{
|
||||
enum xz_ret ret;
|
||||
|
||||
s->in_start = b->in_pos;
|
||||
s->out_start = b->out_pos;
|
||||
|
||||
#ifdef XZ_DEC_BCJ
|
||||
if (s->bcj_active)
|
||||
ret = xz_dec_bcj_run(s->bcj, s->lzma2, b);
|
||||
else
|
||||
#endif
|
||||
ret = xz_dec_lzma2_run(s->lzma2, b);
|
||||
|
||||
s->block.compressed += b->in_pos - s->in_start;
|
||||
s->block.uncompressed += b->out_pos - s->out_start;
|
||||
|
||||
/*
|
||||
* There is no need to separately check for VLI_UNKNOWN, since
|
||||
* the observed sizes are always smaller than VLI_UNKNOWN.
|
||||
*/
|
||||
if (s->block.compressed > s->block_header.compressed
|
||||
|| s->block.uncompressed
|
||||
> s->block_header.uncompressed)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
if (s->check_type == XZ_CHECK_CRC32)
|
||||
s->crc = xz_crc32(b->out + s->out_start,
|
||||
b->out_pos - s->out_start, s->crc);
|
||||
#ifdef XZ_USE_CRC64
|
||||
else if (s->check_type == XZ_CHECK_CRC64)
|
||||
s->crc = xz_crc64(b->out + s->out_start,
|
||||
b->out_pos - s->out_start, s->crc);
|
||||
#endif
|
||||
#ifdef XZ_USE_SHA256
|
||||
else if (s->check_type == XZ_CHECK_SHA256)
|
||||
xz_sha256_update(b->out + s->out_start,
|
||||
b->out_pos - s->out_start, &s->sha256);
|
||||
#endif
|
||||
|
||||
if (ret == XZ_STREAM_END) {
|
||||
if (s->block_header.compressed != VLI_UNKNOWN
|
||||
&& s->block_header.compressed
|
||||
!= s->block.compressed)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
if (s->block_header.uncompressed != VLI_UNKNOWN
|
||||
&& s->block_header.uncompressed
|
||||
!= s->block.uncompressed)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->block.hash.unpadded += s->block_header.size
|
||||
+ s->block.compressed;
|
||||
|
||||
#if defined(XZ_DEC_ANY_CHECK) || defined(XZ_USE_SHA256)
|
||||
s->block.hash.unpadded += check_sizes[s->check_type];
|
||||
#else
|
||||
if (s->check_type == XZ_CHECK_CRC32)
|
||||
s->block.hash.unpadded += 4;
|
||||
else if (IS_CRC64(s->check_type))
|
||||
s->block.hash.unpadded += 8;
|
||||
#endif
|
||||
|
||||
s->block.hash.uncompressed += s->block.uncompressed;
|
||||
s->block.hash.crc32 = xz_crc32(
|
||||
(const uint8_t *)&s->block.hash,
|
||||
sizeof(s->block.hash), s->block.hash.crc32);
|
||||
|
||||
++s->block.count;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Update the Index size and the CRC32 value. */
|
||||
static void index_update(struct xz_dec *s, const struct xz_buf *b)
|
||||
{
|
||||
size_t in_used = b->in_pos - s->in_start;
|
||||
s->index.size += in_used;
|
||||
s->crc = xz_crc32(b->in + s->in_start, in_used, s->crc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode the Number of Records, Unpadded Size, and Uncompressed Size
|
||||
* fields from the Index field. That is, Index Padding and CRC32 are not
|
||||
* decoded by this function.
|
||||
*
|
||||
* This can return XZ_OK (more input needed), XZ_STREAM_END (everything
|
||||
* successfully decoded), or XZ_DATA_ERROR (input is corrupt).
|
||||
*/
|
||||
static enum xz_ret dec_index(struct xz_dec *s, struct xz_buf *b)
|
||||
{
|
||||
enum xz_ret ret;
|
||||
|
||||
do {
|
||||
ret = dec_vli(s, b->in, &b->in_pos, b->in_size);
|
||||
if (ret != XZ_STREAM_END) {
|
||||
index_update(s, b);
|
||||
return ret;
|
||||
}
|
||||
|
||||
switch (s->index.sequence) {
|
||||
case SEQ_INDEX_COUNT:
|
||||
s->index.count = s->vli;
|
||||
|
||||
/*
|
||||
* Validate that the Number of Records field
|
||||
* indicates the same number of Records as
|
||||
* there were Blocks in the Stream.
|
||||
*/
|
||||
if (s->index.count != s->block.count)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->index.sequence = SEQ_INDEX_UNPADDED;
|
||||
break;
|
||||
|
||||
case SEQ_INDEX_UNPADDED:
|
||||
s->index.hash.unpadded += s->vli;
|
||||
s->index.sequence = SEQ_INDEX_UNCOMPRESSED;
|
||||
break;
|
||||
|
||||
case SEQ_INDEX_UNCOMPRESSED:
|
||||
s->index.hash.uncompressed += s->vli;
|
||||
s->index.hash.crc32 = xz_crc32(
|
||||
(const uint8_t *)&s->index.hash,
|
||||
sizeof(s->index.hash),
|
||||
s->index.hash.crc32);
|
||||
--s->index.count;
|
||||
s->index.sequence = SEQ_INDEX_UNPADDED;
|
||||
break;
|
||||
}
|
||||
} while (s->index.count > 0);
|
||||
|
||||
return XZ_STREAM_END;
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate that the next four or eight input bytes match the value
|
||||
* of s->crc. s->pos must be zero when starting to validate the first byte.
|
||||
* The "bits" argument allows using the same code for both CRC32 and CRC64.
|
||||
*/
|
||||
static enum xz_ret crc_validate(struct xz_dec *s, struct xz_buf *b,
|
||||
uint32_t bits)
|
||||
{
|
||||
do {
|
||||
if (b->in_pos == b->in_size)
|
||||
return XZ_OK;
|
||||
|
||||
if (((s->crc >> s->pos) & 0xFF) != b->in[b->in_pos++])
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->pos += 8;
|
||||
|
||||
} while (s->pos < bits);
|
||||
|
||||
s->crc = 0;
|
||||
s->pos = 0;
|
||||
|
||||
return XZ_STREAM_END;
|
||||
}
|
||||
|
||||
#ifdef XZ_DEC_ANY_CHECK
|
||||
/*
|
||||
* Skip over the Check field when the Check ID is not supported.
|
||||
* Returns true once the whole Check field has been skipped over.
|
||||
*/
|
||||
static bool check_skip(struct xz_dec *s, struct xz_buf *b)
|
||||
{
|
||||
while (s->pos < check_sizes[s->check_type]) {
|
||||
if (b->in_pos == b->in_size)
|
||||
return false;
|
||||
|
||||
++b->in_pos;
|
||||
++s->pos;
|
||||
}
|
||||
|
||||
s->pos = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Decode the Stream Header field (the first 12 bytes of the .xz Stream). */
|
||||
static enum xz_ret dec_stream_header(struct xz_dec *s)
|
||||
{
|
||||
if (!memeq(s->temp.buf, HEADER_MAGIC, HEADER_MAGIC_SIZE))
|
||||
return XZ_FORMAT_ERROR;
|
||||
|
||||
if (xz_crc32(s->temp.buf + HEADER_MAGIC_SIZE, 2, 0)
|
||||
!= get_le32(s->temp.buf + HEADER_MAGIC_SIZE + 2))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
if (s->temp.buf[HEADER_MAGIC_SIZE] != 0)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
/*
|
||||
* Of integrity checks, we support none (Check ID = 0),
|
||||
* CRC32 (Check ID = 1), and optionally CRC64 (Check ID = 4).
|
||||
* However, if XZ_DEC_ANY_CHECK is defined, we will accept other
|
||||
* check types too, but then the check won't be verified and
|
||||
* a warning (XZ_UNSUPPORTED_CHECK) will be given.
|
||||
*/
|
||||
if (s->temp.buf[HEADER_MAGIC_SIZE + 1] > XZ_CHECK_MAX)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
s->check_type = s->temp.buf[HEADER_MAGIC_SIZE + 1];
|
||||
|
||||
if (s->check_type > XZ_CHECK_CRC32 && !IS_CRC64(s->check_type)
|
||||
&& !IS_SHA256(s->check_type)) {
|
||||
#ifdef XZ_DEC_ANY_CHECK
|
||||
return XZ_UNSUPPORTED_CHECK;
|
||||
#else
|
||||
return XZ_OPTIONS_ERROR;
|
||||
#endif
|
||||
}
|
||||
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
/* Decode the Stream Footer field (the last 12 bytes of the .xz Stream) */
|
||||
static enum xz_ret dec_stream_footer(struct xz_dec *s)
|
||||
{
|
||||
if (!memeq(s->temp.buf + 10, FOOTER_MAGIC, FOOTER_MAGIC_SIZE))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
if (xz_crc32(s->temp.buf + 4, 6, 0) != get_le32(s->temp.buf))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
/*
|
||||
* Validate Backward Size. Note that we never added the size of the
|
||||
* Index CRC32 field to s->index.size, thus we use s->index.size / 4
|
||||
* instead of s->index.size / 4 - 1.
|
||||
*/
|
||||
if ((s->index.size >> 2) != get_le32(s->temp.buf + 4))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
if (s->temp.buf[8] != 0 || s->temp.buf[9] != s->check_type)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
/*
|
||||
* Use XZ_STREAM_END instead of XZ_OK to be more convenient
|
||||
* for the caller.
|
||||
*/
|
||||
return XZ_STREAM_END;
|
||||
}
|
||||
|
||||
/* Decode the Block Header and initialize the filter chain. */
|
||||
static enum xz_ret dec_block_header(struct xz_dec *s)
|
||||
{
|
||||
enum xz_ret ret;
|
||||
|
||||
/*
|
||||
* Validate the CRC32. We know that the temp buffer is at least
|
||||
* eight bytes so this is safe.
|
||||
*/
|
||||
s->temp.size -= 4;
|
||||
if (xz_crc32(s->temp.buf, s->temp.size, 0)
|
||||
!= get_le32(s->temp.buf + s->temp.size))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->temp.pos = 2;
|
||||
|
||||
/*
|
||||
* Catch unsupported Block Flags. We support only one or two filters
|
||||
* in the chain, so we catch that with the same test.
|
||||
*/
|
||||
#ifdef XZ_DEC_BCJ
|
||||
if (s->temp.buf[1] & 0x3E)
|
||||
#else
|
||||
if (s->temp.buf[1] & 0x3F)
|
||||
#endif
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
/* Compressed Size */
|
||||
if (s->temp.buf[1] & 0x40) {
|
||||
if (dec_vli(s, s->temp.buf, &s->temp.pos, s->temp.size)
|
||||
!= XZ_STREAM_END)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->block_header.compressed = s->vli;
|
||||
} else {
|
||||
s->block_header.compressed = VLI_UNKNOWN;
|
||||
}
|
||||
|
||||
/* Uncompressed Size */
|
||||
if (s->temp.buf[1] & 0x80) {
|
||||
if (dec_vli(s, s->temp.buf, &s->temp.pos, s->temp.size)
|
||||
!= XZ_STREAM_END)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->block_header.uncompressed = s->vli;
|
||||
} else {
|
||||
s->block_header.uncompressed = VLI_UNKNOWN;
|
||||
}
|
||||
|
||||
#ifdef XZ_DEC_BCJ
|
||||
/* If there are two filters, the first one must be a BCJ filter. */
|
||||
s->bcj_active = s->temp.buf[1] & 0x01;
|
||||
if (s->bcj_active) {
|
||||
if (s->temp.size - s->temp.pos < 2)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
ret = xz_dec_bcj_reset(s->bcj, s->temp.buf[s->temp.pos++]);
|
||||
if (ret != XZ_OK)
|
||||
return ret;
|
||||
|
||||
/*
|
||||
* We don't support custom start offset,
|
||||
* so Size of Properties must be zero.
|
||||
*/
|
||||
if (s->temp.buf[s->temp.pos++] != 0x00)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Valid Filter Flags always take at least two bytes. */
|
||||
if (s->temp.size - s->temp.pos < 2)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
/* Filter ID = LZMA2 */
|
||||
if (s->temp.buf[s->temp.pos++] != 0x21)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
/* Size of Properties = 1-byte Filter Properties */
|
||||
if (s->temp.buf[s->temp.pos++] != 0x01)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
/* Filter Properties contains LZMA2 dictionary size. */
|
||||
if (s->temp.size - s->temp.pos < 1)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
ret = xz_dec_lzma2_reset(s->lzma2, s->temp.buf[s->temp.pos++]);
|
||||
if (ret != XZ_OK)
|
||||
return ret;
|
||||
|
||||
/* The rest must be Header Padding. */
|
||||
while (s->temp.pos < s->temp.size)
|
||||
if (s->temp.buf[s->temp.pos++] != 0x00)
|
||||
return XZ_OPTIONS_ERROR;
|
||||
|
||||
s->temp.pos = 0;
|
||||
s->block.compressed = 0;
|
||||
s->block.uncompressed = 0;
|
||||
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
static enum xz_ret dec_main(struct xz_dec *s, struct xz_buf *b)
|
||||
{
|
||||
enum xz_ret ret;
|
||||
|
||||
/*
|
||||
* Store the start position for the case when we are in the middle
|
||||
* of the Index field.
|
||||
*/
|
||||
s->in_start = b->in_pos;
|
||||
|
||||
while (true) {
|
||||
switch (s->sequence) {
|
||||
case SEQ_STREAM_HEADER:
|
||||
/*
|
||||
* Stream Header is copied to s->temp, and then
|
||||
* decoded from there. This way if the caller
|
||||
* gives us only little input at a time, we can
|
||||
* still keep the Stream Header decoding code
|
||||
* simple. Similar approach is used in many places
|
||||
* in this file.
|
||||
*/
|
||||
if (!fill_temp(s, b))
|
||||
return XZ_OK;
|
||||
|
||||
/*
|
||||
* If dec_stream_header() returns
|
||||
* XZ_UNSUPPORTED_CHECK, it is still possible
|
||||
* to continue decoding if working in multi-call
|
||||
* mode. Thus, update s->sequence before calling
|
||||
* dec_stream_header().
|
||||
*/
|
||||
s->sequence = SEQ_BLOCK_START;
|
||||
|
||||
ret = dec_stream_header(s);
|
||||
if (ret != XZ_OK)
|
||||
return ret;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_BLOCK_START:
|
||||
/* We need one byte of input to continue. */
|
||||
if (b->in_pos == b->in_size)
|
||||
return XZ_OK;
|
||||
|
||||
/* See if this is the beginning of the Index field. */
|
||||
if (b->in[b->in_pos] == 0) {
|
||||
s->in_start = b->in_pos++;
|
||||
s->sequence = SEQ_INDEX;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the size of the Block Header and
|
||||
* prepare to decode it.
|
||||
*/
|
||||
s->block_header.size
|
||||
= ((uint32_t)b->in[b->in_pos] + 1) * 4;
|
||||
|
||||
s->temp.size = s->block_header.size;
|
||||
s->temp.pos = 0;
|
||||
s->sequence = SEQ_BLOCK_HEADER;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_BLOCK_HEADER:
|
||||
if (!fill_temp(s, b))
|
||||
return XZ_OK;
|
||||
|
||||
ret = dec_block_header(s);
|
||||
if (ret != XZ_OK)
|
||||
return ret;
|
||||
|
||||
#ifdef XZ_USE_SHA256
|
||||
if (s->check_type == XZ_CHECK_SHA256)
|
||||
xz_sha256_reset(&s->sha256);
|
||||
#endif
|
||||
|
||||
s->sequence = SEQ_BLOCK_UNCOMPRESS;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_BLOCK_UNCOMPRESS:
|
||||
ret = dec_block(s, b);
|
||||
if (ret != XZ_STREAM_END)
|
||||
return ret;
|
||||
|
||||
s->sequence = SEQ_BLOCK_PADDING;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_BLOCK_PADDING:
|
||||
/*
|
||||
* Size of Compressed Data + Block Padding
|
||||
* must be a multiple of four. We don't need
|
||||
* s->block.compressed for anything else
|
||||
* anymore, so we use it here to test the size
|
||||
* of the Block Padding field.
|
||||
*/
|
||||
while (s->block.compressed & 3) {
|
||||
if (b->in_pos == b->in_size)
|
||||
return XZ_OK;
|
||||
|
||||
if (b->in[b->in_pos++] != 0)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
++s->block.compressed;
|
||||
}
|
||||
|
||||
s->sequence = SEQ_BLOCK_CHECK;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_BLOCK_CHECK:
|
||||
if (s->check_type == XZ_CHECK_CRC32) {
|
||||
ret = crc_validate(s, b, 32);
|
||||
if (ret != XZ_STREAM_END)
|
||||
return ret;
|
||||
}
|
||||
else if (IS_CRC64(s->check_type)) {
|
||||
ret = crc_validate(s, b, 64);
|
||||
if (ret != XZ_STREAM_END)
|
||||
return ret;
|
||||
}
|
||||
#ifdef XZ_USE_SHA256
|
||||
else if (s->check_type == XZ_CHECK_SHA256) {
|
||||
s->temp.size = 32;
|
||||
if (!fill_temp(s, b))
|
||||
return XZ_OK;
|
||||
|
||||
if (!xz_sha256_validate(s->temp.buf,
|
||||
&s->sha256))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->pos = 0;
|
||||
}
|
||||
#endif
|
||||
#ifdef XZ_DEC_ANY_CHECK
|
||||
else if (!check_skip(s, b)) {
|
||||
return XZ_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
s->sequence = SEQ_BLOCK_START;
|
||||
break;
|
||||
|
||||
case SEQ_INDEX:
|
||||
ret = dec_index(s, b);
|
||||
if (ret != XZ_STREAM_END)
|
||||
return ret;
|
||||
|
||||
s->sequence = SEQ_INDEX_PADDING;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_INDEX_PADDING:
|
||||
while ((s->index.size + (b->in_pos - s->in_start))
|
||||
& 3) {
|
||||
if (b->in_pos == b->in_size) {
|
||||
index_update(s, b);
|
||||
return XZ_OK;
|
||||
}
|
||||
|
||||
if (b->in[b->in_pos++] != 0)
|
||||
return XZ_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* Finish the CRC32 value and Index size. */
|
||||
index_update(s, b);
|
||||
|
||||
/* Compare the hashes to validate the Index field. */
|
||||
if (!memeq(&s->block.hash, &s->index.hash,
|
||||
sizeof(s->block.hash)))
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
s->sequence = SEQ_INDEX_CRC32;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_INDEX_CRC32:
|
||||
ret = crc_validate(s, b, 32);
|
||||
if (ret != XZ_STREAM_END)
|
||||
return ret;
|
||||
|
||||
s->temp.size = STREAM_HEADER_SIZE;
|
||||
s->sequence = SEQ_STREAM_FOOTER;
|
||||
|
||||
fallthrough;
|
||||
|
||||
case SEQ_STREAM_FOOTER:
|
||||
if (!fill_temp(s, b))
|
||||
return XZ_OK;
|
||||
|
||||
return dec_stream_footer(s);
|
||||
|
||||
case SEQ_STREAM_PADDING:
|
||||
/* Never reached, only silencing a warning */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Never reached */
|
||||
}
|
||||
|
||||
/*
|
||||
* xz_dec_run() is a wrapper for dec_main() to handle some special cases in
|
||||
* multi-call and single-call decoding.
|
||||
*
|
||||
* In multi-call mode, we must return XZ_BUF_ERROR when it seems clear that we
|
||||
* are not going to make any progress anymore. This is to prevent the caller
|
||||
* from calling us infinitely when the input file is truncated or otherwise
|
||||
* corrupt. Since zlib-style API allows that the caller fills the input buffer
|
||||
* only when the decoder doesn't produce any new output, we have to be careful
|
||||
* to avoid returning XZ_BUF_ERROR too easily: XZ_BUF_ERROR is returned only
|
||||
* after the second consecutive call to xz_dec_run() that makes no progress.
|
||||
*
|
||||
* In single-call mode, if we couldn't decode everything and no error
|
||||
* occurred, either the input is truncated or the output buffer is too small.
|
||||
* Since we know that the last input byte never produces any output, we know
|
||||
* that if all the input was consumed and decoding wasn't finished, the file
|
||||
* must be corrupt. Otherwise the output buffer has to be too small or the
|
||||
* file is corrupt in a way that decoding it produces too big output.
|
||||
*
|
||||
* If single-call decoding fails, we reset b->in_pos and b->out_pos back to
|
||||
* their original values. This is because with some filter chains there won't
|
||||
* be any valid uncompressed data in the output buffer unless the decoding
|
||||
* actually succeeds (that's the price to pay of using the output buffer as
|
||||
* the workspace).
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_run(struct xz_dec *s, struct xz_buf *b)
|
||||
{
|
||||
size_t in_start;
|
||||
size_t out_start;
|
||||
enum xz_ret ret;
|
||||
|
||||
if (DEC_IS_SINGLE(s->mode))
|
||||
xz_dec_reset(s);
|
||||
|
||||
in_start = b->in_pos;
|
||||
out_start = b->out_pos;
|
||||
ret = dec_main(s, b);
|
||||
|
||||
if (DEC_IS_SINGLE(s->mode)) {
|
||||
if (ret == XZ_OK)
|
||||
ret = b->in_pos == b->in_size
|
||||
? XZ_DATA_ERROR : XZ_BUF_ERROR;
|
||||
|
||||
if (ret != XZ_STREAM_END) {
|
||||
b->in_pos = in_start;
|
||||
b->out_pos = out_start;
|
||||
}
|
||||
|
||||
} else if (ret == XZ_OK && in_start == b->in_pos
|
||||
&& out_start == b->out_pos) {
|
||||
if (s->allow_buf_error)
|
||||
ret = XZ_BUF_ERROR;
|
||||
|
||||
s->allow_buf_error = true;
|
||||
} else {
|
||||
s->allow_buf_error = false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef XZ_DEC_CONCATENATED
|
||||
XZ_EXTERN enum xz_ret xz_dec_catrun(struct xz_dec *s, struct xz_buf *b,
|
||||
int finish)
|
||||
{
|
||||
enum xz_ret ret;
|
||||
|
||||
if (DEC_IS_SINGLE(s->mode)) {
|
||||
xz_dec_reset(s);
|
||||
finish = true;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (s->sequence == SEQ_STREAM_PADDING) {
|
||||
/*
|
||||
* Skip Stream Padding. Its size must be a multiple
|
||||
* of four bytes which is tracked with s->pos.
|
||||
*/
|
||||
while (true) {
|
||||
if (b->in_pos == b->in_size) {
|
||||
/*
|
||||
* Note that if we are repeatedly
|
||||
* given no input and finish is false,
|
||||
* we will keep returning XZ_OK even
|
||||
* though no progress is being made.
|
||||
* The lack of XZ_BUF_ERROR support
|
||||
* isn't a problem here because a
|
||||
* reasonable caller will eventually
|
||||
* provide more input or set finish
|
||||
* to true.
|
||||
*/
|
||||
if (!finish)
|
||||
return XZ_OK;
|
||||
|
||||
if (s->pos != 0)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
return XZ_STREAM_END;
|
||||
}
|
||||
|
||||
if (b->in[b->in_pos] != 0x00) {
|
||||
if (s->pos != 0)
|
||||
return XZ_DATA_ERROR;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
++b->in_pos;
|
||||
s->pos = (s->pos + 1) & 3;
|
||||
}
|
||||
|
||||
/*
|
||||
* More input remains. It should be a new Stream.
|
||||
*
|
||||
* In single-call mode xz_dec_run() will always call
|
||||
* xz_dec_reset(). Thus, we need to do it here only
|
||||
* in multi-call mode.
|
||||
*/
|
||||
if (DEC_IS_MULTI(s->mode))
|
||||
xz_dec_reset(s);
|
||||
}
|
||||
|
||||
ret = xz_dec_run(s, b);
|
||||
|
||||
if (ret != XZ_STREAM_END)
|
||||
break;
|
||||
|
||||
s->sequence = SEQ_STREAM_PADDING;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
XZ_EXTERN struct xz_dec *xz_dec_init(enum xz_mode mode, uint32_t dict_max)
|
||||
{
|
||||
struct xz_dec *s = kmalloc(sizeof(*s), GFP_KERNEL);
|
||||
if (s == NULL)
|
||||
return NULL;
|
||||
|
||||
s->mode = mode;
|
||||
|
||||
#ifdef XZ_DEC_BCJ
|
||||
s->bcj = xz_dec_bcj_create(DEC_IS_SINGLE(mode));
|
||||
if (s->bcj == NULL)
|
||||
goto error_bcj;
|
||||
#endif
|
||||
|
||||
s->lzma2 = xz_dec_lzma2_create(mode, dict_max);
|
||||
if (s->lzma2 == NULL)
|
||||
goto error_lzma2;
|
||||
|
||||
xz_dec_reset(s);
|
||||
return s;
|
||||
|
||||
error_lzma2:
|
||||
#ifdef XZ_DEC_BCJ
|
||||
xz_dec_bcj_end(s->bcj);
|
||||
error_bcj:
|
||||
#endif
|
||||
kfree(s);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
XZ_EXTERN void xz_dec_reset(struct xz_dec *s)
|
||||
{
|
||||
s->sequence = SEQ_STREAM_HEADER;
|
||||
s->allow_buf_error = false;
|
||||
s->pos = 0;
|
||||
s->crc = 0;
|
||||
memzero(&s->block, sizeof(s->block));
|
||||
memzero(&s->index, sizeof(s->index));
|
||||
s->temp.pos = 0;
|
||||
s->temp.size = STREAM_HEADER_SIZE;
|
||||
}
|
||||
|
||||
XZ_EXTERN void xz_dec_end(struct xz_dec *s)
|
||||
{
|
||||
if (s != NULL) {
|
||||
xz_dec_lzma2_end(s->lzma2);
|
||||
#ifdef XZ_DEC_BCJ
|
||||
xz_dec_bcj_end(s->bcj);
|
||||
#endif
|
||||
kfree(s);
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/* SPDX-License-Identifier: 0BSD */
|
||||
|
||||
/*
|
||||
* LZMA2 definitions
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <https://7-zip.org/>
|
||||
*/
|
||||
|
||||
#ifndef XZ_LZMA2_H
|
||||
#define XZ_LZMA2_H
|
||||
|
||||
/* Range coder constants */
|
||||
#define RC_SHIFT_BITS 8
|
||||
#define RC_TOP_BITS 24
|
||||
#define RC_TOP_VALUE (1 << RC_TOP_BITS)
|
||||
#define RC_BIT_MODEL_TOTAL_BITS 11
|
||||
#define RC_BIT_MODEL_TOTAL (1 << RC_BIT_MODEL_TOTAL_BITS)
|
||||
#define RC_MOVE_BITS 5
|
||||
|
||||
/*
|
||||
* Maximum number of position states. A position state is the lowest pb
|
||||
* number of bits of the current uncompressed offset. In some places there
|
||||
* are different sets of probabilities for different position states.
|
||||
*/
|
||||
#define POS_STATES_MAX (1 << 4)
|
||||
|
||||
/*
|
||||
* This enum is used to track which LZMA symbols have occurred most recently
|
||||
* and in which order. This information is used to predict the next symbol.
|
||||
*
|
||||
* Symbols:
|
||||
* - Literal: One 8-bit byte
|
||||
* - Match: Repeat a chunk of data at some distance
|
||||
* - Long repeat: Multi-byte match at a recently seen distance
|
||||
* - Short repeat: One-byte repeat at a recently seen distance
|
||||
*
|
||||
* The symbol names are in from STATE_oldest_older_previous. REP means
|
||||
* either short or long repeated match, and NONLIT means any non-literal.
|
||||
*/
|
||||
enum lzma_state {
|
||||
STATE_LIT_LIT,
|
||||
STATE_MATCH_LIT_LIT,
|
||||
STATE_REP_LIT_LIT,
|
||||
STATE_SHORTREP_LIT_LIT,
|
||||
STATE_MATCH_LIT,
|
||||
STATE_REP_LIT,
|
||||
STATE_SHORTREP_LIT,
|
||||
STATE_LIT_MATCH,
|
||||
STATE_LIT_LONGREP,
|
||||
STATE_LIT_SHORTREP,
|
||||
STATE_NONLIT_MATCH,
|
||||
STATE_NONLIT_REP
|
||||
};
|
||||
|
||||
/* Total number of states */
|
||||
#define STATES 12
|
||||
|
||||
/* The lowest 7 states indicate that the previous state was a literal. */
|
||||
#define LIT_STATES 7
|
||||
|
||||
/* Indicate that the latest symbol was a literal. */
|
||||
static inline void lzma_state_literal(enum lzma_state *state)
|
||||
{
|
||||
if (*state <= STATE_SHORTREP_LIT_LIT)
|
||||
*state = STATE_LIT_LIT;
|
||||
else if (*state <= STATE_LIT_SHORTREP)
|
||||
*state -= 3;
|
||||
else
|
||||
*state -= 6;
|
||||
}
|
||||
|
||||
/* Indicate that the latest symbol was a match. */
|
||||
static inline void lzma_state_match(enum lzma_state *state)
|
||||
{
|
||||
*state = *state < LIT_STATES ? STATE_LIT_MATCH : STATE_NONLIT_MATCH;
|
||||
}
|
||||
|
||||
/* Indicate that the latest state was a long repeated match. */
|
||||
static inline void lzma_state_long_rep(enum lzma_state *state)
|
||||
{
|
||||
*state = *state < LIT_STATES ? STATE_LIT_LONGREP : STATE_NONLIT_REP;
|
||||
}
|
||||
|
||||
/* Indicate that the latest symbol was a short match. */
|
||||
static inline void lzma_state_short_rep(enum lzma_state *state)
|
||||
{
|
||||
*state = *state < LIT_STATES ? STATE_LIT_SHORTREP : STATE_NONLIT_REP;
|
||||
}
|
||||
|
||||
/* Test if the previous symbol was a literal. */
|
||||
static inline bool lzma_state_is_literal(enum lzma_state state)
|
||||
{
|
||||
return state < LIT_STATES;
|
||||
}
|
||||
|
||||
/* Each literal coder is divided in three sections:
|
||||
* - 0x001-0x0FF: Without match byte
|
||||
* - 0x101-0x1FF: With match byte; match bit is 0
|
||||
* - 0x201-0x2FF: With match byte; match bit is 1
|
||||
*
|
||||
* Match byte is used when the previous LZMA symbol was something else than
|
||||
* a literal (that is, it was some kind of match).
|
||||
*/
|
||||
#define LITERAL_CODER_SIZE 0x300
|
||||
|
||||
/* Maximum number of literal coders */
|
||||
#define LITERAL_CODERS_MAX (1 << 4)
|
||||
|
||||
/* Minimum length of a match is two bytes. */
|
||||
#define MATCH_LEN_MIN 2
|
||||
|
||||
/* Match length is encoded with 4, 5, or 10 bits.
|
||||
*
|
||||
* Length Bits
|
||||
* 2-9 4 = Choice=0 + 3 bits
|
||||
* 10-17 5 = Choice=1 + Choice2=0 + 3 bits
|
||||
* 18-273 10 = Choice=1 + Choice2=1 + 8 bits
|
||||
*/
|
||||
#define LEN_LOW_BITS 3
|
||||
#define LEN_LOW_SYMBOLS (1 << LEN_LOW_BITS)
|
||||
#define LEN_MID_BITS 3
|
||||
#define LEN_MID_SYMBOLS (1 << LEN_MID_BITS)
|
||||
#define LEN_HIGH_BITS 8
|
||||
#define LEN_HIGH_SYMBOLS (1 << LEN_HIGH_BITS)
|
||||
#define LEN_SYMBOLS (LEN_LOW_SYMBOLS + LEN_MID_SYMBOLS + LEN_HIGH_SYMBOLS)
|
||||
|
||||
/*
|
||||
* Maximum length of a match is 273 which is a result of the encoding
|
||||
* described above.
|
||||
*/
|
||||
#define MATCH_LEN_MAX (MATCH_LEN_MIN + LEN_SYMBOLS - 1)
|
||||
|
||||
/*
|
||||
* Different sets of probabilities are used for match distances that have
|
||||
* very short match length: Lengths of 2, 3, and 4 bytes have a separate
|
||||
* set of probabilities for each length. The matches with longer length
|
||||
* use a shared set of probabilities.
|
||||
*/
|
||||
#define DIST_STATES 4
|
||||
|
||||
/*
|
||||
* Get the index of the appropriate probability array for decoding
|
||||
* the distance slot.
|
||||
*/
|
||||
static inline uint32_t lzma_get_dist_state(uint32_t len)
|
||||
{
|
||||
return len < DIST_STATES + MATCH_LEN_MIN
|
||||
? len - MATCH_LEN_MIN : DIST_STATES - 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* The highest two bits of a 32-bit match distance are encoded using six bits.
|
||||
* This six-bit value is called a distance slot. This way encoding a 32-bit
|
||||
* value takes 6-36 bits, larger values taking more bits.
|
||||
*/
|
||||
#define DIST_SLOT_BITS 6
|
||||
#define DIST_SLOTS (1 << DIST_SLOT_BITS)
|
||||
|
||||
/* Match distances up to 127 are fully encoded using probabilities. Since
|
||||
* the highest two bits (distance slot) are always encoded using six bits,
|
||||
* the distances 0-3 don't need any additional bits to encode, since the
|
||||
* distance slot itself is the same as the actual distance. DIST_MODEL_START
|
||||
* indicates the first distance slot where at least one additional bit is
|
||||
* needed.
|
||||
*/
|
||||
#define DIST_MODEL_START 4
|
||||
|
||||
/*
|
||||
* Match distances greater than 127 are encoded in three pieces:
|
||||
* - distance slot: the highest two bits
|
||||
* - direct bits: 2-26 bits below the highest two bits
|
||||
* - alignment bits: four lowest bits
|
||||
*
|
||||
* Direct bits don't use any probabilities.
|
||||
*
|
||||
* The distance slot value of 14 is for distances 128-191.
|
||||
*/
|
||||
#define DIST_MODEL_END 14
|
||||
|
||||
/* Distance slots that indicate a distance <= 127. */
|
||||
#define FULL_DISTANCES_BITS (DIST_MODEL_END / 2)
|
||||
#define FULL_DISTANCES (1 << FULL_DISTANCES_BITS)
|
||||
|
||||
/*
|
||||
* For match distances greater than 127, only the highest two bits and the
|
||||
* lowest four bits (alignment) is encoded using probabilities.
|
||||
*/
|
||||
#define ALIGN_BITS 4
|
||||
#define ALIGN_SIZE (1 << ALIGN_BITS)
|
||||
#define ALIGN_MASK (ALIGN_SIZE - 1)
|
||||
|
||||
/* Total number of all probability variables */
|
||||
#define PROBS_TOTAL (1846 + LITERAL_CODERS_MAX * LITERAL_CODER_SIZE)
|
||||
|
||||
/*
|
||||
* LZMA remembers the four most recent match distances. Reusing these
|
||||
* distances tends to take less space than re-encoding the actual
|
||||
* distance value.
|
||||
*/
|
||||
#define REPS 4
|
||||
|
||||
#endif
|
||||
@@ -1,189 +0,0 @@
|
||||
/* SPDX-License-Identifier: 0BSD */
|
||||
|
||||
/*
|
||||
* Private includes and definitions
|
||||
*
|
||||
* Author: Lasse Collin <lasse.collin@tukaani.org>
|
||||
*/
|
||||
|
||||
#ifndef XZ_PRIVATE_H
|
||||
#define XZ_PRIVATE_H
|
||||
|
||||
#ifdef __KERNEL__
|
||||
# include <linux/xz.h>
|
||||
# include <linux/kernel.h>
|
||||
# include <linux/unaligned.h>
|
||||
/* XZ_PREBOOT may be defined only via decompress_unxz.c. */
|
||||
# ifndef XZ_PREBOOT
|
||||
# include <linux/slab.h>
|
||||
# include <linux/vmalloc.h>
|
||||
# include <linux/string.h>
|
||||
# ifdef CONFIG_XZ_DEC_X86
|
||||
# define XZ_DEC_X86
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_POWERPC
|
||||
# define XZ_DEC_POWERPC
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_IA64
|
||||
# define XZ_DEC_IA64
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_ARM
|
||||
# define XZ_DEC_ARM
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_ARMTHUMB
|
||||
# define XZ_DEC_ARMTHUMB
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_SPARC
|
||||
# define XZ_DEC_SPARC
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_ARM64
|
||||
# define XZ_DEC_ARM64
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_RISCV
|
||||
# define XZ_DEC_RISCV
|
||||
# endif
|
||||
# ifdef CONFIG_XZ_DEC_MICROLZMA
|
||||
# define XZ_DEC_MICROLZMA
|
||||
# endif
|
||||
# define memeq(a, b, size) (memcmp(a, b, size) == 0)
|
||||
# define memzero(buf, size) memset(buf, 0, size)
|
||||
# endif
|
||||
# define get_le32(p) le32_to_cpup((const uint32_t *)(p))
|
||||
#else
|
||||
/*
|
||||
* For userspace builds, use a separate header to define the required
|
||||
* macros and functions. This makes it easier to adapt the code into
|
||||
* different environments and avoids clutter in the Linux kernel tree.
|
||||
*/
|
||||
# include "xz_config.h"
|
||||
#endif
|
||||
|
||||
/* If no specific decoding mode is requested, enable support for all modes. */
|
||||
#if !defined(XZ_DEC_SINGLE) && !defined(XZ_DEC_PREALLOC) \
|
||||
&& !defined(XZ_DEC_DYNALLOC)
|
||||
# define XZ_DEC_SINGLE
|
||||
# define XZ_DEC_PREALLOC
|
||||
# define XZ_DEC_DYNALLOC
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The DEC_IS_foo(mode) macros are used in "if" statements. If only some
|
||||
* of the supported modes are enabled, these macros will evaluate to true or
|
||||
* false at compile time and thus allow the compiler to omit unneeded code.
|
||||
*/
|
||||
#ifdef XZ_DEC_SINGLE
|
||||
# define DEC_IS_SINGLE(mode) ((mode) == XZ_SINGLE)
|
||||
#else
|
||||
# define DEC_IS_SINGLE(mode) (false)
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_PREALLOC
|
||||
# define DEC_IS_PREALLOC(mode) ((mode) == XZ_PREALLOC)
|
||||
#else
|
||||
# define DEC_IS_PREALLOC(mode) (false)
|
||||
#endif
|
||||
|
||||
#ifdef XZ_DEC_DYNALLOC
|
||||
# define DEC_IS_DYNALLOC(mode) ((mode) == XZ_DYNALLOC)
|
||||
#else
|
||||
# define DEC_IS_DYNALLOC(mode) (false)
|
||||
#endif
|
||||
|
||||
#if !defined(XZ_DEC_SINGLE)
|
||||
# define DEC_IS_MULTI(mode) (true)
|
||||
#elif defined(XZ_DEC_PREALLOC) || defined(XZ_DEC_DYNALLOC)
|
||||
# define DEC_IS_MULTI(mode) ((mode) != XZ_SINGLE)
|
||||
#else
|
||||
# define DEC_IS_MULTI(mode) (false)
|
||||
#endif
|
||||
|
||||
/*
|
||||
* If any of the BCJ filter decoders are wanted, define XZ_DEC_BCJ.
|
||||
* XZ_DEC_BCJ is used to enable generic support for BCJ decoders.
|
||||
*/
|
||||
#ifndef XZ_DEC_BCJ
|
||||
# if defined(XZ_DEC_X86) || defined(XZ_DEC_POWERPC) \
|
||||
|| defined(XZ_DEC_IA64) \
|
||||
|| defined(XZ_DEC_ARM) || defined(XZ_DEC_ARMTHUMB) \
|
||||
|| defined(XZ_DEC_SPARC) || defined(XZ_DEC_ARM64) \
|
||||
|| defined(XZ_DEC_RISCV)
|
||||
# define XZ_DEC_BCJ
|
||||
# endif
|
||||
#endif
|
||||
|
||||
struct xz_sha256 {
|
||||
/* Buffered input data */
|
||||
uint8_t data[64];
|
||||
|
||||
/* Internal state and the final hash value */
|
||||
uint32_t state[8];
|
||||
|
||||
/* Size of the input data */
|
||||
uint64_t size;
|
||||
};
|
||||
|
||||
/* Reset the SHA-256 state to prepare for a new calculation. */
|
||||
XZ_EXTERN void xz_sha256_reset(struct xz_sha256 *s);
|
||||
|
||||
/* Update the SHA-256 state with new data. */
|
||||
XZ_EXTERN void xz_sha256_update(const uint8_t *buf, size_t size,
|
||||
struct xz_sha256 *s);
|
||||
|
||||
/*
|
||||
* Finish the SHA-256 calculation. Compare the result with the first 32 bytes
|
||||
* from buf. Return true if the values are equal and false if they aren't.
|
||||
*/
|
||||
XZ_EXTERN bool xz_sha256_validate(const uint8_t *buf, struct xz_sha256 *s);
|
||||
|
||||
/*
|
||||
* Allocate memory for LZMA2 decoder. xz_dec_lzma2_reset() must be used
|
||||
* before calling xz_dec_lzma2_run().
|
||||
*/
|
||||
XZ_EXTERN struct xz_dec_lzma2 *xz_dec_lzma2_create(enum xz_mode mode,
|
||||
uint32_t dict_max);
|
||||
|
||||
/*
|
||||
* Decode the LZMA2 properties (one byte) and reset the decoder. Return
|
||||
* XZ_OK on success, XZ_MEMLIMIT_ERROR if the preallocated dictionary is not
|
||||
* big enough, and XZ_OPTIONS_ERROR if props indicates something that this
|
||||
* decoder doesn't support.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_lzma2_reset(struct xz_dec_lzma2 *s,
|
||||
uint8_t props);
|
||||
|
||||
/* Decode raw LZMA2 stream from b->in to b->out. */
|
||||
XZ_EXTERN enum xz_ret xz_dec_lzma2_run(struct xz_dec_lzma2 *s,
|
||||
struct xz_buf *b);
|
||||
|
||||
/* Free the memory allocated for the LZMA2 decoder. */
|
||||
XZ_EXTERN void xz_dec_lzma2_end(struct xz_dec_lzma2 *s);
|
||||
|
||||
#ifdef XZ_DEC_BCJ
|
||||
/*
|
||||
* Allocate memory for BCJ decoders. xz_dec_bcj_reset() must be used before
|
||||
* calling xz_dec_bcj_run().
|
||||
*/
|
||||
XZ_EXTERN struct xz_dec_bcj *xz_dec_bcj_create(bool single_call);
|
||||
|
||||
/*
|
||||
* Decode the Filter ID of a BCJ filter. This implementation doesn't
|
||||
* support custom start offsets, so no decoding of Filter Properties
|
||||
* is needed. Returns XZ_OK if the given Filter ID is supported.
|
||||
* Otherwise XZ_OPTIONS_ERROR is returned.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_bcj_reset(struct xz_dec_bcj *s, uint8_t id);
|
||||
|
||||
/*
|
||||
* Decode raw BCJ + LZMA2 stream. This must be used only if there actually is
|
||||
* a BCJ filter in the chain. If the chain has only LZMA2, xz_dec_lzma2_run()
|
||||
* must be called directly.
|
||||
*/
|
||||
XZ_EXTERN enum xz_ret xz_dec_bcj_run(struct xz_dec_bcj *s,
|
||||
struct xz_dec_lzma2 *lzma2,
|
||||
struct xz_buf *b);
|
||||
|
||||
/* Free the memory allocated for the BCJ filters. */
|
||||
#define xz_dec_bcj_end(s) kfree(s)
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,182 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
/*
|
||||
* SHA-256
|
||||
*
|
||||
* This is based on the XZ Utils version which is based public domain code
|
||||
* from Crypto++ Library 5.5.1 released in 2007: https://www.cryptopp.com/
|
||||
*
|
||||
* Authors: Wei Dai
|
||||
* Lasse Collin <lasse.collin@tukaani.org>
|
||||
*/
|
||||
|
||||
#include "xz_private.h"
|
||||
|
||||
static inline uint32_t
|
||||
rotr_32(uint32_t num, unsigned amount)
|
||||
{
|
||||
return (num >> amount) | (num << (32 - amount));
|
||||
}
|
||||
|
||||
#define blk0(i) (W[i] = get_be32(&data[4 * i]))
|
||||
#define blk2(i) (W[i & 15] += s1(W[(i - 2) & 15]) + W[(i - 7) & 15] \
|
||||
+ s0(W[(i - 15) & 15]))
|
||||
|
||||
#define Ch(x, y, z) (z ^ (x & (y ^ z)))
|
||||
#define Maj(x, y, z) ((x & (y ^ z)) + (y & z))
|
||||
|
||||
#define a(i) T[(0 - i) & 7]
|
||||
#define b(i) T[(1 - i) & 7]
|
||||
#define c(i) T[(2 - i) & 7]
|
||||
#define d(i) T[(3 - i) & 7]
|
||||
#define e(i) T[(4 - i) & 7]
|
||||
#define f(i) T[(5 - i) & 7]
|
||||
#define g(i) T[(6 - i) & 7]
|
||||
#define h(i) T[(7 - i) & 7]
|
||||
|
||||
#define R(i, j, blk) \
|
||||
h(i) += S1(e(i)) + Ch(e(i), f(i), g(i)) + SHA256_K[i + j] + blk; \
|
||||
d(i) += h(i); \
|
||||
h(i) += S0(a(i)) + Maj(a(i), b(i), c(i))
|
||||
#define R0(i) R(i, 0, blk0(i))
|
||||
#define R2(i) R(i, j, blk2(i))
|
||||
|
||||
#define S0(x) rotr_32(x ^ rotr_32(x ^ rotr_32(x, 9), 11), 2)
|
||||
#define S1(x) rotr_32(x ^ rotr_32(x ^ rotr_32(x, 14), 5), 6)
|
||||
#define s0(x) (rotr_32(x ^ rotr_32(x, 11), 7) ^ (x >> 3))
|
||||
#define s1(x) (rotr_32(x ^ rotr_32(x, 2), 17) ^ (x >> 10))
|
||||
|
||||
static const uint32_t SHA256_K[64] = {
|
||||
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,
|
||||
0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
|
||||
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
|
||||
0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
|
||||
0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC,
|
||||
0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
||||
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7,
|
||||
0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
|
||||
0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,
|
||||
0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
|
||||
0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3,
|
||||
0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
||||
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5,
|
||||
0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
|
||||
0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,
|
||||
0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
|
||||
};
|
||||
|
||||
static void
|
||||
transform(uint32_t state[8], const uint8_t data[64])
|
||||
{
|
||||
uint32_t W[16];
|
||||
uint32_t T[8];
|
||||
unsigned int j;
|
||||
|
||||
/* Copy state[] to working vars. */
|
||||
memcpy(T, state, sizeof(T));
|
||||
|
||||
/* The first 16 operations unrolled */
|
||||
R0( 0); R0( 1); R0( 2); R0( 3);
|
||||
R0( 4); R0( 5); R0( 6); R0( 7);
|
||||
R0( 8); R0( 9); R0(10); R0(11);
|
||||
R0(12); R0(13); R0(14); R0(15);
|
||||
|
||||
/* The remaining 48 operations partially unrolled */
|
||||
for (j = 16; j < 64; j += 16) {
|
||||
R2( 0); R2( 1); R2( 2); R2( 3);
|
||||
R2( 4); R2( 5); R2( 6); R2( 7);
|
||||
R2( 8); R2( 9); R2(10); R2(11);
|
||||
R2(12); R2(13); R2(14); R2(15);
|
||||
}
|
||||
|
||||
/* Add the working vars back into state[]. */
|
||||
state[0] += a(0);
|
||||
state[1] += b(0);
|
||||
state[2] += c(0);
|
||||
state[3] += d(0);
|
||||
state[4] += e(0);
|
||||
state[5] += f(0);
|
||||
state[6] += g(0);
|
||||
state[7] += h(0);
|
||||
}
|
||||
|
||||
XZ_EXTERN void xz_sha256_reset(struct xz_sha256 *s)
|
||||
{
|
||||
static const uint32_t initial_state[8] = {
|
||||
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
|
||||
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19
|
||||
};
|
||||
|
||||
memcpy(s->state, initial_state, sizeof(initial_state));
|
||||
s->size = 0;
|
||||
}
|
||||
|
||||
XZ_EXTERN void xz_sha256_update(const uint8_t *buf, size_t size,
|
||||
struct xz_sha256 *s)
|
||||
{
|
||||
size_t copy_start;
|
||||
size_t copy_size;
|
||||
|
||||
/*
|
||||
* Copy the input data into a properly aligned temporary buffer.
|
||||
* This way we can be called with arbitrarily sized buffers
|
||||
* (no need to be a multiple of 64 bytes).
|
||||
*
|
||||
* Full 64-byte chunks could be processed directly from buf with
|
||||
* unaligned access. It seemed to make very little difference in
|
||||
* speed on x86-64 though. Thus it was omitted.
|
||||
*/
|
||||
while (size > 0) {
|
||||
copy_start = s->size & 0x3F;
|
||||
copy_size = 64 - copy_start;
|
||||
if (copy_size > size)
|
||||
copy_size = size;
|
||||
|
||||
memcpy(s->data + copy_start, buf, copy_size);
|
||||
|
||||
buf += copy_size;
|
||||
size -= copy_size;
|
||||
s->size += copy_size;
|
||||
|
||||
if ((s->size & 0x3F) == 0)
|
||||
transform(s->state, s->data);
|
||||
}
|
||||
}
|
||||
|
||||
XZ_EXTERN bool xz_sha256_validate(const uint8_t *buf, struct xz_sha256 *s)
|
||||
{
|
||||
/*
|
||||
* Add padding as described in RFC 3174 (it describes SHA-1 but
|
||||
* the same padding style is used for SHA-256 too).
|
||||
*/
|
||||
size_t i = s->size & 0x3F;
|
||||
s->data[i++] = 0x80;
|
||||
|
||||
while (i != 64 - 8) {
|
||||
if (i == 64) {
|
||||
transform(s->state, s->data);
|
||||
i = 0;
|
||||
}
|
||||
|
||||
s->data[i++] = 0x00;
|
||||
}
|
||||
|
||||
/* Convert the message size from bytes to bits. */
|
||||
s->size *= 8;
|
||||
|
||||
/*
|
||||
* Store the message size in big endian byte order and
|
||||
* calculate the final hash value.
|
||||
*/
|
||||
for (i = 0; i < 8; ++i)
|
||||
s->data[64 - 8 + i] = (uint8_t)(s->size >> ((7 - i) * 8));
|
||||
|
||||
transform(s->state, s->data);
|
||||
|
||||
/* Compare if the hash value matches the first 32 bytes in buf. */
|
||||
for (i = 0; i < 8; ++i)
|
||||
if (get_unaligned_be32(buf + 4 * i) != s->state[i])
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/* SPDX-License-Identifier: 0BSD */
|
||||
|
||||
/*
|
||||
* Definitions for handling the .xz file format
|
||||
*
|
||||
* Author: Lasse Collin <lasse.collin@tukaani.org>
|
||||
*/
|
||||
|
||||
#ifndef XZ_STREAM_H
|
||||
#define XZ_STREAM_H
|
||||
|
||||
#if defined(__KERNEL__) && !XZ_INTERNAL_CRC32
|
||||
# include <linux/crc32.h>
|
||||
# undef crc32
|
||||
# define xz_crc32(buf, size, crc) \
|
||||
(~crc32_le(~(uint32_t)(crc), buf, size))
|
||||
#endif
|
||||
|
||||
/*
|
||||
* See the .xz file format specification at
|
||||
* https://tukaani.org/xz/xz-file-format.txt
|
||||
* to understand the container format.
|
||||
*/
|
||||
|
||||
#define STREAM_HEADER_SIZE 12
|
||||
|
||||
#define HEADER_MAGIC "\3757zXZ"
|
||||
#define HEADER_MAGIC_SIZE 6
|
||||
|
||||
#define FOOTER_MAGIC "YZ"
|
||||
#define FOOTER_MAGIC_SIZE 2
|
||||
|
||||
/*
|
||||
* Variable-length integer can hold a 63-bit unsigned integer or a special
|
||||
* value indicating that the value is unknown.
|
||||
*
|
||||
* Experimental: vli_type can be defined to uint32_t to save a few bytes
|
||||
* in code size (no effect on speed). Doing so limits the uncompressed and
|
||||
* compressed size of the file to less than 256 MiB and may also weaken
|
||||
* error detection slightly.
|
||||
*/
|
||||
typedef uint64_t vli_type;
|
||||
|
||||
#define VLI_MAX ((vli_type)-1 / 2)
|
||||
#define VLI_UNKNOWN ((vli_type)-1)
|
||||
|
||||
/* Maximum encoded size of a VLI */
|
||||
#define VLI_BYTES_MAX (sizeof(vli_type) * 8 / 7)
|
||||
|
||||
/* Integrity Check types */
|
||||
enum xz_check {
|
||||
XZ_CHECK_NONE = 0,
|
||||
XZ_CHECK_CRC32 = 1,
|
||||
XZ_CHECK_CRC64 = 4,
|
||||
XZ_CHECK_SHA256 = 10
|
||||
};
|
||||
|
||||
/* Maximum possible Check ID */
|
||||
#define XZ_CHECK_MAX 15
|
||||
|
||||
#endif
|
||||
@@ -34,6 +34,7 @@ import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -103,19 +104,20 @@ 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 me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
||||
import me.kavishdevar.librepods.screens.AppSettingsScreen
|
||||
import me.kavishdevar.librepods.screens.DebugScreen
|
||||
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
||||
import me.kavishdevar.librepods.screens.LongPress
|
||||
// import me.kavishdevar.librepods.screens.Onboarding
|
||||
import me.kavishdevar.librepods.screens.Onboarding
|
||||
import me.kavishdevar.librepods.screens.RenameScreen
|
||||
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.utils.CrossDevice
|
||||
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
lateinit var serviceConnection: ServiceConnection
|
||||
@@ -140,6 +142,8 @@ class MainActivity : ComponentActivity() {
|
||||
Main()
|
||||
}
|
||||
}
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -174,6 +178,73 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent) {
|
||||
val data: Uri? = intent.data
|
||||
|
||||
if (data != null && data.scheme == "librepods") {
|
||||
when (data.host) {
|
||||
"add-magic-keys" -> {
|
||||
// Extract query parameters
|
||||
val queryParams = data.queryParameterNames
|
||||
queryParams.forEach { param ->
|
||||
val value = data.getQueryParameter(param)
|
||||
// Handle your parameters here
|
||||
Log.d("LibrePods", "Parameter: $param = $value")
|
||||
}
|
||||
|
||||
// Process the magic keys addition
|
||||
handleAddMagicKeys(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddMagicKeys(uri: Uri) {
|
||||
val context = this
|
||||
val sharedPreferences = getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
val irkHex = uri.getQueryParameter("irk")
|
||||
val encKeyHex = uri.getQueryParameter("enc_key")
|
||||
|
||||
try {
|
||||
if (irkHex != null && validateHexInput(irkHex)) {
|
||||
val irkBytes = hexStringToByteArray(irkHex)
|
||||
val irkBase64 = Base64.encode(irkBytes)
|
||||
sharedPreferences.edit().putString("IRK", irkBase64).apply()
|
||||
}
|
||||
|
||||
if (encKeyHex != null && validateHexInput(encKeyHex)) {
|
||||
val encKeyBytes = hexStringToByteArray(encKeyHex)
|
||||
val encKeyBase64 = Base64.encode(encKeyBytes)
|
||||
sharedPreferences.edit().putString("ENC_KEY", encKeyBase64).apply()
|
||||
}
|
||||
|
||||
Toast.makeText(this, "Magic keys added successfully!", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Error processing magic keys: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateHexInput(input: String): Boolean {
|
||||
val hexPattern = Regex("^[0-9a-fA-F]{32}$")
|
||||
return hexPattern.matches(input)
|
||||
}
|
||||
|
||||
private fun hexStringToByteArray(hex: String): ByteArray {
|
||||
val result = ByteArray(16)
|
||||
for (i in 0 until 16) {
|
||||
val hexByte = hex.substring(i * 2, i * 2 + 2)
|
||||
result[i] = hexByte.toInt(16).toByte()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag")
|
||||
@@ -182,7 +253,7 @@ class MainActivity : ComponentActivity() {
|
||||
fun Main() {
|
||||
val isConnected = remember { mutableStateOf(false) }
|
||||
val isRemotelyConnected = remember { mutableStateOf(false) }
|
||||
// val hookAvailable = RadareOffsetFinder(LocalContext.current).isHookOffsetAvailable()
|
||||
val hookAvailable = RadareOffsetFinder(LocalContext.current).isHookOffsetAvailable()
|
||||
val context = LocalContext.current
|
||||
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
||||
val overlaySkipped = remember { mutableStateOf(context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("overlay_permission_skipped", false)) }
|
||||
@@ -243,7 +314,7 @@ fun Main() {
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "settings", // if (hookAvailable) "settings" else "onboarding",
|
||||
startDestination = if (hookAvailable) "settings" else "onboarding",
|
||||
enterTransition = {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it },
|
||||
@@ -301,9 +372,9 @@ fun Main() {
|
||||
composable("head_tracking") {
|
||||
HeadTrackingScreen(navController)
|
||||
}
|
||||
// composable("onboarding") {
|
||||
// Onboarding(navController, context)
|
||||
// }
|
||||
composable("onboarding") {
|
||||
Onboarding(navController, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,4 +732,3 @@ fun PermissionCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,22 +62,20 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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 kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.composables.AdaptiveRainbowBrush
|
||||
import me.kavishdevar.librepods.composables.IconAreaSize
|
||||
import me.kavishdevar.librepods.composables.ControlCenterNoiseControlSegmentedButton
|
||||
import me.kavishdevar.librepods.composables.IconAreaSize
|
||||
import me.kavishdevar.librepods.composables.VerticalVolumeSlider
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.Battery
|
||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.utils.Battery
|
||||
import me.kavishdevar.librepods.utils.BatteryComponent
|
||||
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -56,7 +56,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
||||
|
||||
private val ContainerColor = Color(0x593C3C3E)
|
||||
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
|
||||
|
||||
@@ -127,4 +127,4 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam
|
||||
@Composable
|
||||
fun IndependentTogglePreview() {
|
||||
IndependentToggle("Test", AirPodsService(), "test", LocalContext.current.getSharedPreferences("preview", 0), true)
|
||||
}
|
||||
}
|
||||
@@ -73,10 +73,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
@@ -57,6 +58,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.StemAction
|
||||
|
||||
@Composable
|
||||
fun PressAndHoldSettings(navController: NavController) {
|
||||
@@ -70,6 +72,24 @@ fun PressAndHoldSettings(navController: NavController) {
|
||||
val animatedLeftBackgroundColor by animateColorAsState(targetValue = leftBackgroundColor, animationSpec = animationSpec)
|
||||
val animatedRightBackgroundColor by animateColorAsState(targetValue = rightBackgroundColor, animationSpec = animationSpec)
|
||||
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
val leftAction = sharedPreferences.getString("left_long_press_action", StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
||||
val rightAction = sharedPreferences.getString("right_long_press_action", StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
||||
|
||||
val leftActionText = when (StemAction.valueOf(leftAction ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) {
|
||||
StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control)
|
||||
StemAction.DIGITAL_ASSISTANT -> "Digital Assistant"
|
||||
else -> "INVALID!!"
|
||||
}
|
||||
|
||||
val rightActionText = when (StemAction.valueOf(rightAction ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) {
|
||||
StemAction.CYCLE_NOISE_CONTROL_MODES -> stringResource(R.string.noise_control)
|
||||
StemAction.DIGITAL_ASSISTANT -> "Digital Assistant"
|
||||
else -> "INVALID!!"
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.press_and_hold_airpods).uppercase(),
|
||||
style = TextStyle(
|
||||
@@ -122,7 +142,7 @@ fun PressAndHoldSettings(navController: NavController) {
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = stringResource(R.string.noise_control),
|
||||
text = leftActionText,
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
@@ -182,7 +202,7 @@ fun PressAndHoldSettings(navController: NavController) {
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = stringResource(R.string.noise_control),
|
||||
text = rightActionText,
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package me.kavishdevar.librepods.utils
|
||||
package me.kavishdevar.librepods.constants
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
@@ -27,27 +24,10 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class Enums(val value: ByteArray) {
|
||||
NOISE_CANCELLATION(Capabilities.NOISE_CANCELLATION),
|
||||
CONVERSATION_AWARENESS(Capabilities.CONVERSATION_AWARENESS),
|
||||
CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY(Capabilities.CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY),
|
||||
PREFIX(byteArrayOf(0x04, 0x00, 0x04, 0x00)),
|
||||
SETTINGS(byteArrayOf(0x09, 0x00)),
|
||||
SUFFIX(byteArrayOf(0x00, 0x00, 0x00)),
|
||||
NOTIFICATION_FILTER(byteArrayOf(0x0f)),
|
||||
HANDSHAKE(byteArrayOf(0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)),
|
||||
SPECIFIC_FEATURES(byteArrayOf(0x4d)),
|
||||
SET_SPECIFIC_FEATURES(PREFIX.value + SPECIFIC_FEATURES.value + byteArrayOf(0x00,
|
||||
0xff.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)),
|
||||
REQUEST_NOTIFICATIONS(PREFIX.value + NOTIFICATION_FILTER.value + byteArrayOf(0x00, 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())),
|
||||
NOISE_CANCELLATION_PREFIX(PREFIX.value + SETTINGS.value + NOISE_CANCELLATION.value),
|
||||
NOISE_CANCELLATION_OFF(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.OFF.value + SUFFIX.value),
|
||||
NOISE_CANCELLATION_ON(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.ON.value + SUFFIX.value),
|
||||
NOISE_CANCELLATION_TRANSPARENCY(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.TRANSPARENCY.value + SUFFIX.value),
|
||||
NOISE_CANCELLATION_ADAPTIVE(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.ADAPTIVE.value + SUFFIX.value),
|
||||
SET_CONVERSATION_AWARENESS_OFF(PREFIX.value + SETTINGS.value + CONVERSATION_AWARENESS.value + Capabilities.ConversationAwareness.OFF.value + SUFFIX.value),
|
||||
SET_CONVERSATION_AWARENESS_ON(PREFIX.value + SETTINGS.value + CONVERSATION_AWARENESS.value + Capabilities.ConversationAwareness.ON.value + SUFFIX.value),
|
||||
CONVERSATION_AWARENESS_RECEIVE_PREFIX(PREFIX.value + byteArrayOf(0x4b, 0x00, 0x02, 0x00)),
|
||||
START_HEAD_TRACKING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x08, 0xA1.toByte(), 0x02, 0x42, 0x0B, 0x08, 0x0E, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x40, 0x9C.toByte(), 0x00, 0x00)),
|
||||
STOP_HEAD_TRACKING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x10, 0x00, 0x11, 0x00, 0x08, 0x7E.toByte(), 0x10, 0x02, 0x42, 0x0B, 0x08, 0x4E.toByte(), 0x10, 0x02, 0x1A, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00));
|
||||
}
|
||||
|
||||
object BatteryComponent {
|
||||
@@ -156,7 +136,7 @@ class AirPodsNotifications {
|
||||
}
|
||||
|
||||
val name: String =
|
||||
when (status) {
|
||||
when (status) {
|
||||
1 -> "OFF"
|
||||
2 -> "ON"
|
||||
3 -> "TRANSPARENCY"
|
||||
@@ -251,103 +231,10 @@ class AirPodsNotifications {
|
||||
class Capabilities {
|
||||
companion object {
|
||||
val NOISE_CANCELLATION = byteArrayOf(0x0d)
|
||||
val CONVERSATION_AWARENESS = byteArrayOf(0x28)
|
||||
val CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY = byteArrayOf(0x01, 0x02)
|
||||
val EAR_DETECTION = byteArrayOf(0x06)
|
||||
}
|
||||
|
||||
enum class NoiseCancellation(val value: ByteArray) {
|
||||
OFF(byteArrayOf(0x01)),
|
||||
ON(byteArrayOf(0x02)),
|
||||
TRANSPARENCY(byteArrayOf(0x03)),
|
||||
ADAPTIVE(byteArrayOf(0x04));
|
||||
}
|
||||
|
||||
enum class ConversationAwareness(val value: ByteArray) {
|
||||
OFF(byteArrayOf(0x02)),
|
||||
ON(byteArrayOf(0x01));
|
||||
}
|
||||
}
|
||||
|
||||
enum class LongPressPackets(val value: ByteArray) {
|
||||
ENABLE_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0F, 0x00, 0x00, 0x00)),
|
||||
|
||||
DISABLE_OFF_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
|
||||
DISABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)),
|
||||
DISABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
|
||||
DISABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)),
|
||||
|
||||
ENABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
|
||||
ENABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
|
||||
ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
|
||||
|
||||
DISABLE_TRANSPARENCY_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
|
||||
DISABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)),
|
||||
DISABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)),
|
||||
DISABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)),
|
||||
|
||||
ENABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
|
||||
ENABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
|
||||
ENABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
|
||||
|
||||
DISABLE_ANC_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0D, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ANC_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x05, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)),
|
||||
|
||||
ENABLE_ANC_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
|
||||
ENABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
|
||||
ENABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
|
||||
|
||||
DISABLE_ADAPTIVE_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x05, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ADAPTIVE_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)),
|
||||
|
||||
ENABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
|
||||
ENABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
|
||||
ENABLE_ADAPTIVE_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
|
||||
|
||||
ENABLE_EVERYTHING_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0E, 0x00, 0x00, 0x00)),
|
||||
DISABLE_TRANSPARENCY_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0A, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ADAPTIVE_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
|
||||
DISABLE_ANC_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0C, 0x00, 0x00, 0x00)),
|
||||
}
|
||||
|
||||
//enum class LongPressMode {
|
||||
// OFF, TRANSPARENCY, ADAPTIVE, ANC
|
||||
//}
|
||||
//
|
||||
//data class LongPressPacket(val modes: Set<LongPressMode>) {
|
||||
// val value: ByteArray
|
||||
// get() {
|
||||
// val baseArray = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A)
|
||||
// val modeByte = calculateModeByte()
|
||||
// return baseArray + byteArrayOf(modeByte, 0x00, 0x00, 0x00)
|
||||
// }
|
||||
//
|
||||
// private fun calculateModeByte(): Byte {
|
||||
// var modeByte: Byte = 0x00
|
||||
// modes.forEach { mode ->
|
||||
// modeByte = when (mode) {
|
||||
// LongPressMode.OFF -> (modeByte + 0x01).toByte()
|
||||
// LongPressMode.TRANSPARENCY -> (modeByte + 0x02).toByte()
|
||||
// LongPressMode.ADAPTIVE -> (modeByte + 0x04).toByte()
|
||||
// LongPressMode.ANC -> (modeByte + 0x08).toByte()
|
||||
// }
|
||||
// }
|
||||
// return modeByte
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//fun determinePacket(changedIndex: Int, newEnabled: Boolean, oldModes: Set<LongPressMode>, newModes: Set<LongPressMode>): ByteArray? {
|
||||
// return if (newEnabled) {
|
||||
// LongPressPacket(oldModes + newModes.elementAt(changedIndex)).value
|
||||
// } else {
|
||||
// LongPressPacket(oldModes - newModes.elementAt(changedIndex)).value
|
||||
// }
|
||||
//}
|
||||
|
||||
fun isHeadTrackingData(data: ByteArray): Boolean {
|
||||
if (data.size <= 60) return false
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.constants
|
||||
|
||||
import me.kavishdevar.librepods.constants.StemAction.entries
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
|
||||
enum class StemAction {
|
||||
PLAY_PAUSE,
|
||||
PREVIOUS_TRACK,
|
||||
NEXT_TRACK,
|
||||
CAMERA_SHUTTER,
|
||||
DIGITAL_ASSISTANT,
|
||||
CYCLE_NOISE_CONTROL_MODES;
|
||||
companion object {
|
||||
fun fromString(action: String): StemAction? {
|
||||
return entries.find { it.name == action }
|
||||
}
|
||||
val defaultActions: Map<AACPManager.Companion.StemPressType, StemAction> = mapOf(
|
||||
AACPManager.Companion.StemPressType.SINGLE_PRESS to PLAY_PAUSE,
|
||||
AACPManager.Companion.StemPressType.DOUBLE_PRESS to NEXT_TRACK,
|
||||
AACPManager.Companion.StemPressType.TRIPLE_PRESS to PREVIOUS_TRACK,
|
||||
AACPManager.Companion.StemPressType.LONG_PRESS to CYCLE_NOISE_CONTROL_MODES,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -99,10 +99,10 @@ import me.kavishdevar.librepods.composables.NameField
|
||||
import me.kavishdevar.librepods.composables.NavigationButton
|
||||
import me.kavishdevar.librepods.composables.NoiseControlSettings
|
||||
import me.kavishdevar.librepods.composables.PressAndHoldSettings
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||
@@ -113,6 +113,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
var isLocallyConnected by remember { mutableStateOf(isConnected) }
|
||||
var isRemotelyConnected by remember { mutableStateOf(isRemotelyConnected) }
|
||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val bleOnlyMode = sharedPreferences.getBoolean("ble_only_mode", false)
|
||||
var device by remember { mutableStateOf(dev) }
|
||||
var deviceName by remember {
|
||||
mutableStateOf(
|
||||
@@ -329,35 +330,67 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
NameField(
|
||||
name = stringResource(R.string.name),
|
||||
value = deviceName.text,
|
||||
navController = navController
|
||||
)
|
||||
// Show BLE-only mode indicator
|
||||
if (bleOnlyMode) {
|
||||
Text(
|
||||
text = "BLE-only mode - advanced features disabled",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
NoiseControlSettings(service = service)
|
||||
// Only show name field when not in BLE-only mode
|
||||
if (!bleOnlyMode) {
|
||||
NameField(
|
||||
name = stringResource(R.string.name),
|
||||
value = deviceName.text,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.head_gestures).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
// Only show L2CAP-dependent features when not in BLE-only mode
|
||||
if (!bleOnlyMode) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
NoiseControlSettings(service = service)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
NavigationButton(to = "head_tracking", "Head Tracking", navController)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.head_gestures).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PressAndHoldSettings(navController = navController)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
NavigationButton(to = "head_tracking", "Head Tracking", navController)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AudioSettings()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PressAndHoldSettings(navController = navController)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AudioSettings()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
name = "Off Listening Mode",
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = false,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AccessibilitySettings()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
@@ -365,23 +398,15 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
service = service,
|
||||
functionName = "setEarDetection",
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = true
|
||||
default = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
IndependentToggle(
|
||||
name = "Off Listening Mode",
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = false,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
)
|
||||
// Only show debug when not in BLE-only mode
|
||||
if (!bleOnlyMode) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
NavigationButton("debug", "Debug", navController)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AccessibilitySettings()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
NavigationButton("debug", "Debug", navController)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
//import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.roundToInt
|
||||
@@ -179,6 +179,21 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
mutableStateOf(sharedPreferences.getBoolean("takeover_when_media_start", true))
|
||||
}
|
||||
|
||||
var useAlternateHeadTrackingPackets by remember {
|
||||
mutableStateOf(sharedPreferences.getBoolean("use_alternate_head_tracking_packets", false))
|
||||
}
|
||||
|
||||
var bleOnlyMode by remember {
|
||||
mutableStateOf(sharedPreferences.getBoolean("ble_only_mode", false))
|
||||
}
|
||||
|
||||
// Ensure the default value is properly set if not exists
|
||||
LaunchedEffect(Unit) {
|
||||
if (!sharedPreferences.contains("ble_only_mode")) {
|
||||
sharedPreferences.edit().putBoolean("ble_only_mode", false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
var mDensity by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
fun validateHexInput(input: String): Boolean {
|
||||
@@ -331,6 +346,69 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Connection Mode".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
bleOnlyMode = !bleOnlyMode
|
||||
sharedPreferences.edit().putBoolean("ble_only_mode", bleOnlyMode).apply()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "BLE Only Mode",
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Text(
|
||||
text = "Only use Bluetooth Low Energy for battery data and ear detection. Disables advanced features requiring L2CAP connection.",
|
||||
fontSize = 13.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
StyledSwitch(
|
||||
checked = bleOnlyMode,
|
||||
onCheckedChange = {
|
||||
bleOnlyMode = it
|
||||
sharedPreferences.edit().putBoolean("ble_only_mode", it).apply()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Conversational Awareness".uppercase(),
|
||||
style = TextStyle(
|
||||
@@ -1040,6 +1118,47 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
useAlternateHeadTrackingPackets = !useAlternateHeadTrackingPackets
|
||||
sharedPreferences.edit().putBoolean("use_alternate_head_tracking_packets", useAlternateHeadTrackingPackets).apply()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Use alternate head tracking packets",
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Enable this if head tracking doesn't work for you. This sends different data to AirPods for requesting/stopping head tracking data.",
|
||||
fontSize = 14.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
StyledSwitch(
|
||||
checked = useAlternateHeadTrackingPackets,
|
||||
onCheckedChange = {
|
||||
useAlternateHeadTrackingPackets = it
|
||||
sharedPreferences.edit().putBoolean("use_alternate_head_tracking_packets", it).apply()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1107,68 +1226,68 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// if (showResetDialog) {
|
||||
// AlertDialog(
|
||||
// onDismissRequest = { showResetDialog = false },
|
||||
// title = {
|
||||
// Text(
|
||||
// "Reset Hook Offset",
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
// fontWeight = FontWeight.Medium
|
||||
// )
|
||||
// },
|
||||
// text = {
|
||||
// Text(
|
||||
// "This will clear the current hook offset and require you to go through the setup process again. Are you sure you want to continue?",
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
// )
|
||||
// },
|
||||
// confirmButton = {
|
||||
// TextButton(
|
||||
// onClick = {
|
||||
// if (RadareOffsetFinder.clearHookOffsets()) {
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// "Hook offset has been reset. Redirecting to setup...",
|
||||
// Toast.LENGTH_LONG
|
||||
// ).show()
|
||||
//
|
||||
// navController.navigate("onboarding") {
|
||||
// popUpTo("settings") { inclusive = true }
|
||||
// }
|
||||
// } else {
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// "Failed to reset hook offset",
|
||||
// Toast.LENGTH_SHORT
|
||||
// ).show()
|
||||
// }
|
||||
// showResetDialog = false
|
||||
// },
|
||||
// colors = ButtonDefaults.textButtonColors(
|
||||
// contentColor = MaterialTheme.colorScheme.error
|
||||
// )
|
||||
// ) {
|
||||
// Text(
|
||||
// "Reset",
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
// fontWeight = FontWeight.Medium
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// dismissButton = {
|
||||
// TextButton(
|
||||
// onClick = { showResetDialog = false }
|
||||
// ) {
|
||||
// Text(
|
||||
// "Cancel",
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
// fontWeight = FontWeight.Medium
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
if (showResetDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showResetDialog = false },
|
||||
title = {
|
||||
Text(
|
||||
"Reset Hook Offset",
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
"This will clear the current hook offset and require you to go through the setup process again. Are you sure you want to continue?",
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (RadareOffsetFinder.clearHookOffsets()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Hook offset has been reset. Redirecting to setup...",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
navController.navigate("onboarding") {
|
||||
popUpTo("settings") { inclusive = true }
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Failed to reset hook offset",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
showResetDialog = false
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
"Reset",
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { showResetDialog = false }
|
||||
) {
|
||||
Text(
|
||||
"Cancel",
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showIrkDialog) {
|
||||
AlertDialog(
|
||||
|
||||
@@ -100,9 +100,9 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||
import me.kavishdevar.librepods.utils.isHeadTrackingData
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
data class PacketInfo(
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Onboarding(navController: NavController, activityContext: Context) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
|
||||
val radareOffsetFinder = remember { RadareOffsetFinder(activityContext) }
|
||||
val progressState by radareOffsetFinder.progressState.collectAsState()
|
||||
var isComplete by remember { mutableStateOf(false) }
|
||||
var hasStarted by remember { mutableStateOf(false) }
|
||||
var rootCheckPassed by remember { mutableStateOf(false) }
|
||||
var checkingRoot by remember { mutableStateOf(false) }
|
||||
var rootCheckFailed by remember { mutableStateOf(false) }
|
||||
var moduleEnabled by remember { mutableStateOf(false) }
|
||||
var bluetoothToggled by remember { mutableStateOf(false) }
|
||||
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showSkipDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun checkRootAccess() {
|
||||
checkingRoot = true
|
||||
rootCheckFailed = false
|
||||
kotlinx.coroutines.MainScope().launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c id")
|
||||
val exitValue = process.waitFor()
|
||||
withContext(Dispatchers.Main) {
|
||||
rootCheckPassed = (exitValue == 0)
|
||||
rootCheckFailed = (exitValue != 0)
|
||||
checkingRoot = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Onboarding", "Root check failed", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
rootCheckPassed = false
|
||||
rootCheckFailed = true
|
||||
checkingRoot = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(hasStarted) {
|
||||
if (hasStarted && rootCheckPassed) {
|
||||
Log.d("Onboarding", "Checking if hook offset is available...")
|
||||
val isHookReady = radareOffsetFinder.isHookOffsetAvailable()
|
||||
Log.d("Onboarding", "Hook offset ready: $isHookReady")
|
||||
|
||||
if (isHookReady) {
|
||||
Log.d("Onboarding", "Hook is ready")
|
||||
isComplete = true
|
||||
} else {
|
||||
Log.d("Onboarding", "Hook not ready, starting setup process...")
|
||||
withContext(Dispatchers.IO) {
|
||||
radareOffsetFinder.setupAndFindOffset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(progressState) {
|
||||
if (progressState is RadareOffsetFinder.ProgressState.Success) {
|
||||
isComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"Setting Up",
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More Options"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Skip Setup") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showSkipDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
containerColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = backgroundColor),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (!rootCheckPassed && !hasStarted) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "Root Access",
|
||||
tint = accentColor,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Root Access Required",
|
||||
style = TextStyle(
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "This app needs root access to hook onto the Bluetooth library",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor.copy(alpha = 0.7f)
|
||||
)
|
||||
)
|
||||
|
||||
if (rootCheckFailed) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Root access was denied. Please grant root permissions.",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = Color(0xFFFF453A)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = { checkRootAccess() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
enabled = !checkingRoot
|
||||
) {
|
||||
if (checkingRoot) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
"Check Root Access",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StatusIcon(if (hasStarted) progressState else RadareOffsetFinder.ProgressState.Idle, isDarkTheme)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
AnimatedContent(
|
||||
targetState = if (hasStarted) getStatusTitle(progressState, isComplete, moduleEnabled, bluetoothToggled) else "Setup Required",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||
) { text ->
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
AnimatedContent(
|
||||
targetState = if (hasStarted)
|
||||
getStatusDescription(progressState, isComplete, moduleEnabled, bluetoothToggled)
|
||||
else
|
||||
"AirPods functionality requires one-time setup for hooking into Bluetooth library",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||
) { text ->
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor.copy(alpha = 0.7f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (!hasStarted) {
|
||||
Button(
|
||||
onClick = { hasStarted = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Start Setup",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when (progressState) {
|
||||
is RadareOffsetFinder.ProgressState.DownloadProgress -> {
|
||||
val progress = (progressState as RadareOffsetFinder.ProgressState.DownloadProgress).progress
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = progress,
|
||||
label = "Download Progress"
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
strokeCap = StrokeCap.Round,
|
||||
color = accentColor
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "${(progress * 100).toInt()}%",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is RadareOffsetFinder.ProgressState.Success -> {
|
||||
if (!moduleEnabled) {
|
||||
Button(
|
||||
onClick = { moduleEnabled = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"I've Enabled/Reactivated the Module",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (!bluetoothToggled) {
|
||||
Button(
|
||||
onClick = { bluetoothToggled = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"I've Toggled Bluetooth",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
navController.navigate("settings") {
|
||||
popUpTo("onboarding") { inclusive = true }
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Continue to Settings",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is RadareOffsetFinder.ProgressState.Idle,
|
||||
is RadareOffsetFinder.ProgressState.Error -> {
|
||||
// No specific UI for these states
|
||||
}
|
||||
else -> {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
strokeCap = StrokeCap.Round,
|
||||
color = accentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (progressState is RadareOffsetFinder.ProgressState.Error && !isComplete && hasStarted) {
|
||||
Button(
|
||||
onClick = {
|
||||
Log.d("Onboarding", "Trying to find offset again...")
|
||||
kotlinx.coroutines.MainScope().launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
radareOffsetFinder.setupAndFindOffset()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Try Again",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSkipDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSkipDialog = false },
|
||||
title = { Text("Skip Setup") },
|
||||
text = {
|
||||
Text(
|
||||
"Have you installed the root module that patches the Bluetooth library directly? This option is for users who have manually patched their system instead of using the dynamic hook.",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
val sharedPreferences = activityContext.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
TextButton(
|
||||
onClick = {
|
||||
showSkipDialog = false
|
||||
RadareOffsetFinder.clearHookOffsets()
|
||||
sharedPreferences.edit().putBoolean("skip_setup", true).apply()
|
||||
navController.navigate("settings") {
|
||||
popUpTo("onboarding") { inclusive = true }
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"Yes, Skip Setup",
|
||||
color = accentColor,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { showSkipDialog = false }
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
containerColor = backgroundColor,
|
||||
textContentColor = textColor,
|
||||
titleContentColor = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusIcon(
|
||||
progressState: RadareOffsetFinder.ProgressState,
|
||||
isDarkTheme: Boolean
|
||||
) {
|
||||
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
val errorColor = if (isDarkTheme) Color(0xFFFF453A) else Color(0xFFFF3B30)
|
||||
val successColor = if (isDarkTheme) Color(0xFF30D158) else Color(0xFF34C759)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(80.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (progressState) {
|
||||
is RadareOffsetFinder.ProgressState.Error -> {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = "Error",
|
||||
tint = errorColor,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
}
|
||||
is RadareOffsetFinder.ProgressState.Success -> {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Success",
|
||||
tint = successColor,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
}
|
||||
is RadareOffsetFinder.ProgressState.Idle -> {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "Settings",
|
||||
tint = accentColor,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(50.dp),
|
||||
color = accentColor,
|
||||
strokeWidth = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusTitle(
|
||||
state: RadareOffsetFinder.ProgressState,
|
||||
isComplete: Boolean,
|
||||
moduleEnabled: Boolean,
|
||||
bluetoothToggled: Boolean
|
||||
): String {
|
||||
return when (state) {
|
||||
is RadareOffsetFinder.ProgressState.Success -> {
|
||||
when {
|
||||
!moduleEnabled -> "Enable Xposed Module"
|
||||
!bluetoothToggled -> "Toggle Bluetooth"
|
||||
else -> "Setup Complete"
|
||||
}
|
||||
}
|
||||
is RadareOffsetFinder.ProgressState.Idle -> "Getting Ready"
|
||||
is RadareOffsetFinder.ProgressState.CheckingExisting -> "Checking if radare2 already downloaded"
|
||||
is RadareOffsetFinder.ProgressState.Downloading -> "Downloading radare2"
|
||||
is RadareOffsetFinder.ProgressState.DownloadProgress -> "Downloading radare2"
|
||||
is RadareOffsetFinder.ProgressState.Extracting -> "Extracting radare2"
|
||||
is RadareOffsetFinder.ProgressState.MakingExecutable -> "Setting executable permissions"
|
||||
is RadareOffsetFinder.ProgressState.FindingOffset -> "Finding function offset"
|
||||
is RadareOffsetFinder.ProgressState.SavingOffset -> "Saving offset"
|
||||
is RadareOffsetFinder.ProgressState.Cleaning -> "Cleaning Up"
|
||||
is RadareOffsetFinder.ProgressState.Error -> "Setup Failed"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusDescription(
|
||||
state: RadareOffsetFinder.ProgressState,
|
||||
isComplete: Boolean,
|
||||
moduleEnabled: Boolean,
|
||||
bluetoothToggled: Boolean
|
||||
): String {
|
||||
return when (state) {
|
||||
is RadareOffsetFinder.ProgressState.Success -> {
|
||||
when {
|
||||
!moduleEnabled -> "Please enable the LibrePods Xposed module in your Xposed manager (e.g. LSPosed). If already enabled, disable and re-enable it."
|
||||
!bluetoothToggled -> "Please turn off and then turn on Bluetooth to apply the changes."
|
||||
else -> "All set! You can now use your AirPods with enhanced functionality."
|
||||
}
|
||||
}
|
||||
is RadareOffsetFinder.ProgressState.Idle -> "Preparing"
|
||||
is RadareOffsetFinder.ProgressState.CheckingExisting -> "Checking if radare2 are already installed"
|
||||
is RadareOffsetFinder.ProgressState.Downloading -> "Starting radare2 download"
|
||||
is RadareOffsetFinder.ProgressState.DownloadProgress -> "Downloading radare2"
|
||||
is RadareOffsetFinder.ProgressState.Extracting -> "Extracting radare2"
|
||||
is RadareOffsetFinder.ProgressState.MakingExecutable -> "Setting executable permissions on radare2 binaries"
|
||||
is RadareOffsetFinder.ProgressState.FindingOffset -> "Looking for the required Bluetooth function in system libraries"
|
||||
is RadareOffsetFinder.ProgressState.SavingOffset -> "Saving the function offset"
|
||||
is RadareOffsetFinder.ProgressState.Cleaning -> "Removing temporary extracted files"
|
||||
is RadareOffsetFinder.ProgressState.Error -> state.message
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun OnboardingPreview() {
|
||||
Onboarding(navController = NavController(LocalContext.current), activityContext = LocalContext.current)
|
||||
}
|
||||
|
||||
private suspend fun delay(timeMillis: Long) {
|
||||
kotlinx.coroutines.delay(timeMillis)
|
||||
}
|
||||
@@ -57,10 +57,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
@@ -69,6 +68,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.StemAction
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import kotlin.experimental.and
|
||||
@@ -84,6 +84,16 @@ fun RightDivider() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable()
|
||||
fun RightDividerNoIcon() {
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LongPress(navController: NavController, name: String) {
|
||||
@@ -104,6 +114,10 @@ fun LongPress(navController: NavController, name: String) {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val deviceName = sharedPreferences.getString("name", "AirPods Pro")
|
||||
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)) }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
@@ -153,56 +167,88 @@ fun LongPress(navController: NavController, name: String) {
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "NOISE CONTROL",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier
|
||||
.padding(8.dp, bottom = 4.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val offListeningModeValue = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||
val offListeningMode = offListeningModeValue == 1.toByte()
|
||||
LongPressElement(
|
||||
name = "Off",
|
||||
enabled = offListeningMode,
|
||||
resourceId = R.drawable.noise_cancellation,
|
||||
isFirst = true)
|
||||
if (offListeningMode) RightDivider()
|
||||
LongPressElement(
|
||||
name = "Transparency",
|
||||
resourceId = R.drawable.transparency,
|
||||
isFirst = !offListeningMode)
|
||||
RightDivider()
|
||||
LongPressElement(
|
||||
name = "Adaptive",
|
||||
resourceId = R.drawable.adaptive)
|
||||
RightDivider()
|
||||
LongPressElement(
|
||||
name = "Noise Cancellation",
|
||||
resourceId = R.drawable.noise_cancellation,
|
||||
isLast = true)
|
||||
LongPressActionElement(
|
||||
name = "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).apply()
|
||||
},
|
||||
isFirst = true,
|
||||
isLast = false
|
||||
)
|
||||
RightDividerNoIcon()
|
||||
LongPressActionElement(
|
||||
name = "Digital Assistant",
|
||||
selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
|
||||
onClick = {
|
||||
longPressAction = StemAction.DIGITAL_ASSISTANT
|
||||
sharedPreferences.edit().putString(prefKey, StemAction.DIGITAL_ASSISTANT.name).apply()
|
||||
},
|
||||
isFirst = false,
|
||||
isLast = true
|
||||
)
|
||||
}
|
||||
|
||||
if (longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES) {
|
||||
Text(
|
||||
text = "NOISE CONTROL",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp, bottom = 4.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val offListeningModeValue = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||
val offListeningMode = offListeningModeValue == 1.toByte()
|
||||
LongPressElement(
|
||||
name = "Off",
|
||||
enabled = offListeningMode,
|
||||
resourceId = R.drawable.noise_cancellation,
|
||||
isFirst = true)
|
||||
if (offListeningMode) RightDivider()
|
||||
LongPressElement(
|
||||
name = "Transparency",
|
||||
resourceId = R.drawable.transparency,
|
||||
isFirst = !offListeningMode)
|
||||
RightDivider()
|
||||
LongPressElement(
|
||||
name = "Adaptive",
|
||||
resourceId = R.drawable.adaptive)
|
||||
RightDivider()
|
||||
LongPressElement(
|
||||
name = "Noise Cancellation",
|
||||
resourceId = R.drawable.noise_cancellation,
|
||||
isLast = true)
|
||||
}
|
||||
Text(
|
||||
"Press and hold the stem to cycle between the selected noise control modes.",
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"Press and hold the stem to cycle between the selected noise control modes.",
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Log.d("PressAndHoldSettingsScreen", "Current byte: ${ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
|
||||
@@ -336,7 +382,7 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Icon(
|
||||
bitmap = ImageBitmap.imageResource(resourceId),
|
||||
painter = painterResource(resourceId),
|
||||
contentDescription = "Icon",
|
||||
tint = Color(0xFF007AFF),
|
||||
modifier = Modifier
|
||||
@@ -384,3 +430,67 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongPressActionElement(
|
||||
name: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
isFirst: Boolean = false,
|
||||
isLast: Boolean = false
|
||||
) {
|
||||
val darkMode = isSystemInDarkTheme()
|
||||
val shape = when {
|
||||
isFirst -> RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
|
||||
isLast -> RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
|
||||
else -> RoundedCornerShape(0.dp)
|
||||
}
|
||||
var backgroundColor by remember { mutableStateOf(if (darkMode) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.background(animatedBackgroundColor, shape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
backgroundColor = if (darkMode) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
backgroundColor = if (darkMode) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
name,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 4.dp)
|
||||
)
|
||||
Checkbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { onClick() },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent,
|
||||
disabledBorderColor = Color.Transparent,
|
||||
disabledCheckedBoxColor = Color.Transparent,
|
||||
disabledUncheckedBoxColor = Color.Transparent,
|
||||
disabledUncheckedBorderColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.scale(1.5f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.NoiseControlMode
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
|
||||
@@ -28,7 +28,6 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
@@ -78,12 +77,15 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import me.kavishdevar.librepods.MainActivity
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.Battery
|
||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||
import me.kavishdevar.librepods.constants.StemAction
|
||||
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
|
||||
import me.kavishdevar.librepods.utils.BLEManager
|
||||
import me.kavishdevar.librepods.utils.Battery
|
||||
import me.kavishdevar.librepods.utils.BatteryComponent
|
||||
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||
import me.kavishdevar.librepods.utils.BluetoothConnectionManager
|
||||
import me.kavishdevar.librepods.utils.CrossDevice
|
||||
import me.kavishdevar.librepods.utils.CrossDevicePackets
|
||||
@@ -112,7 +114,6 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_
|
||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_CHARGING
|
||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ICON
|
||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
|
||||
import me.kavishdevar.librepods.utils.isHeadTrackingData
|
||||
import me.kavishdevar.librepods.widgets.BatteryWidget
|
||||
import me.kavishdevar.librepods.widgets.NoiseControlWidget
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
@@ -143,7 +144,7 @@ object ServiceManager {
|
||||
class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
var macAddress = ""
|
||||
lateinit var aacpManager: AACPManager
|
||||
|
||||
var cameraActive = false
|
||||
data class ServiceConfig(
|
||||
var deviceName: String = "AirPods",
|
||||
var earDetectionEnabled: Boolean = true,
|
||||
@@ -155,6 +156,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
var conversationalAwarenessVolume: Int = 43,
|
||||
var textColor: Long = -1L,
|
||||
var qsClickBehavior: String = "cycle",
|
||||
var bleOnlyMode: Boolean = false,
|
||||
|
||||
// AirPods state-based takeover
|
||||
var takeoverWhenDisconnected: Boolean = true,
|
||||
@@ -164,7 +166,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
// Phone state-based takeover
|
||||
var takeoverWhenRingingCall: Boolean = true,
|
||||
var takeoverWhenMediaStart: Boolean = true
|
||||
var takeoverWhenMediaStart: Boolean = true,
|
||||
|
||||
var leftSinglePressAction: StemAction = StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!,
|
||||
var rightSinglePressAction: StemAction = StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!,
|
||||
|
||||
var leftDoublePressAction: StemAction = StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!,
|
||||
var rightDoublePressAction: StemAction = StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!,
|
||||
|
||||
var leftTriplePressAction: StemAction = StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!,
|
||||
var rightTriplePressAction: StemAction = StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!,
|
||||
|
||||
var leftLongPressAction: StemAction = StemAction.defaultActions[StemPressType.LONG_PRESS]!!,
|
||||
var rightLongPressAction: StemAction = StemAction.defaultActions[StemPressType.LONG_PRESS]!!,
|
||||
)
|
||||
|
||||
private lateinit var config: ServiceConfig
|
||||
@@ -193,13 +207,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
device: BLEManager.AirPodsStatus,
|
||||
previousStatus: BLEManager.AirPodsStatus?
|
||||
) {
|
||||
if (device.connectionState == "Disconnected") {
|
||||
// Store MAC address for BLE-only mode if not already stored
|
||||
if (config.bleOnlyMode && macAddress.isEmpty()) {
|
||||
macAddress = device.address
|
||||
sharedPreferences.edit {
|
||||
putString("mac_address", macAddress)
|
||||
}
|
||||
Log.d("AirPodsBLEService", "BLE-only mode: stored MAC address ${device.address}")
|
||||
}
|
||||
|
||||
if (device.connectionState == "Disconnected" && !config.bleOnlyMode) {
|
||||
Log.d("AirPodsBLEService", "Seems no device has taken over, we will.")
|
||||
val bluetoothManager = getSystemService(BluetoothManager::class.java)
|
||||
val bluetoothAdapter = bluetoothManager.adapter
|
||||
val bluetoothDevice = bluetoothAdapter.getRemoteDevice(sharedPreferences.getString(
|
||||
val bluetoothDevice = bluetoothManager.adapter.getRemoteDevice(sharedPreferences.getString(
|
||||
"mac_address", "") ?: "")
|
||||
connectToSocket(bluetoothAdapter, bluetoothDevice)
|
||||
connectToSocket(bluetoothDevice)
|
||||
}
|
||||
Log.d("AirPodsBLEService", "Device status changed")
|
||||
if (isConnectedLocally) return
|
||||
@@ -261,7 +283,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
leftInEar: Boolean,
|
||||
rightInEar: Boolean
|
||||
) {
|
||||
Log.d("AirPodsBLEService", "Ear state changed")
|
||||
Log.d("AirPodsBLEService", "Ear state changed - Left: $leftInEar, Right: $rightInEar")
|
||||
|
||||
// In BLE-only mode, ear detection is purely based on BLE data
|
||||
if (config.bleOnlyMode) {
|
||||
Log.d("AirPodsBLEService", "BLE-only mode: ear detection from BLE data")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBatteryChanged(device: BLEManager.AirPodsStatus) {
|
||||
@@ -302,6 +329,67 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
fun cameraOpened() {
|
||||
Log.d("AirPodsService", "Camera opened, gonna handle stem presses and take action if enabled")
|
||||
val isCameraShutterUsed = listOf(
|
||||
config.leftSinglePressAction,
|
||||
config.rightSinglePressAction,
|
||||
config.leftDoublePressAction,
|
||||
config.rightDoublePressAction,
|
||||
config.leftTriplePressAction,
|
||||
config.rightTriplePressAction,
|
||||
config.leftLongPressAction,
|
||||
config.rightLongPressAction
|
||||
).any { it == StemAction.CAMERA_SHUTTER }
|
||||
|
||||
if (isCameraShutterUsed) {
|
||||
Log.d("AirPodsService", "Camera opened, setting up stem actions")
|
||||
cameraActive = true
|
||||
setupStemActions(isCameraActive = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun cameraClosed() {
|
||||
cameraActive = false
|
||||
setupStemActions()
|
||||
}
|
||||
|
||||
fun isCustomAction(
|
||||
action: StemAction?,
|
||||
default: StemAction?,
|
||||
isCameraActive: Boolean = false
|
||||
): Boolean {
|
||||
Log.d("AirPodsService", "Checking if action $action is custom against default $default, camera active: $isCameraActive")
|
||||
return action != default && (action != StemAction.CAMERA_SHUTTER || isCameraActive)
|
||||
}
|
||||
|
||||
fun setupStemActions(isCameraActive: Boolean = false) {
|
||||
val singlePressDefault = StemAction.defaultActions[StemPressType.SINGLE_PRESS]
|
||||
val doublePressDefault = StemAction.defaultActions[StemPressType.DOUBLE_PRESS]
|
||||
val triplePressDefault = StemAction.defaultActions[StemPressType.TRIPLE_PRESS]
|
||||
val longPressDefault = StemAction.defaultActions[StemPressType.LONG_PRESS]
|
||||
|
||||
val singlePressCustomized = isCustomAction(config.leftSinglePressAction, singlePressDefault, isCameraActive) ||
|
||||
isCustomAction(config.rightSinglePressAction, singlePressDefault, isCameraActive)
|
||||
val doublePressCustomized = isCustomAction(config.leftDoublePressAction, doublePressDefault, isCameraActive) ||
|
||||
isCustomAction(config.rightDoublePressAction, doublePressDefault, isCameraActive)
|
||||
val triplePressCustomized = isCustomAction(config.leftTriplePressAction, triplePressDefault, isCameraActive) ||
|
||||
isCustomAction(config.rightTriplePressAction, triplePressDefault, isCameraActive)
|
||||
val longPressCustomized = isCustomAction(config.leftLongPressAction, longPressDefault, isCameraActive) ||
|
||||
isCustomAction(config.rightLongPressAction, longPressDefault, isCameraActive)
|
||||
Log.d("AirPodsService", "Setting up stem actions: " +
|
||||
"Single Press Customized: $singlePressCustomized, " +
|
||||
"Double Press Customized: $doublePressCustomized, " +
|
||||
"Triple Press Customized: $triplePressCustomized, " +
|
||||
"Long Press Customized: $longPressCustomized")
|
||||
aacpManager.sendStemConfigPacket(
|
||||
singlePressCustomized,
|
||||
doublePressCustomized,
|
||||
triplePressCustomized,
|
||||
longPressCustomized,
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalEncodingApi
|
||||
private fun initializeAACPManagerCallback() {
|
||||
aacpManager.setPacketCallback(object : AACPManager.PacketCallback {
|
||||
@@ -400,12 +488,58 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStemPressReceived(stemPress: ByteArray) {
|
||||
val (stemPressType, bud) = aacpManager.parseStemPressResponse(stemPress)
|
||||
|
||||
Log.d("AirPodsParser", "Stem press received: $stemPressType on $bud")
|
||||
|
||||
val action = getActionFor(bud, stemPressType)
|
||||
Log.d("AirPodsParser", "$bud $stemPressType action: $action")
|
||||
|
||||
action?.let { executeStemAction(it) }
|
||||
}
|
||||
|
||||
override fun onUnknownPacketReceived(packet: ByteArray) {
|
||||
Log.d("AACPManager", "Unknown packet received: ${packet.joinToString(" ") { "%02X".format(it) }}")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun getActionFor(bud: AACPManager.Companion.StemPressBudType, type: StemPressType): StemAction? {
|
||||
return when (type) {
|
||||
StemPressType.SINGLE_PRESS -> if (bud == AACPManager.Companion.StemPressBudType.LEFT) config.leftSinglePressAction else config.rightSinglePressAction
|
||||
StemPressType.DOUBLE_PRESS -> if (bud == AACPManager.Companion.StemPressBudType.LEFT) config.leftDoublePressAction else config.rightDoublePressAction
|
||||
StemPressType.TRIPLE_PRESS -> if (bud == AACPManager.Companion.StemPressBudType.LEFT) config.leftTriplePressAction else config.rightTriplePressAction
|
||||
StemPressType.LONG_PRESS -> if (bud == AACPManager.Companion.StemPressBudType.LEFT) config.leftLongPressAction else config.rightLongPressAction
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeStemAction(action: StemAction) {
|
||||
when (action) {
|
||||
StemAction.defaultActions[StemPressType.SINGLE_PRESS] -> {
|
||||
Log.d("AirPodsParser", "Default single press action: Play/Pause, not taking action.")
|
||||
}
|
||||
StemAction.PLAY_PAUSE -> MediaController.sendPlayPause()
|
||||
StemAction.PREVIOUS_TRACK -> MediaController.sendPreviousTrack()
|
||||
StemAction.NEXT_TRACK -> MediaController.sendNextTrack()
|
||||
StemAction.CAMERA_SHUTTER -> Runtime.getRuntime().exec(arrayOf("su", "-c", "input keyevent 27"))
|
||||
StemAction.DIGITAL_ASSISTANT -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val intent = Intent(Intent.ACTION_VOICE_COMMAND).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Log.w("AirPodsParser", "Digital Assistant action is not supported on this Android version.")
|
||||
}
|
||||
}
|
||||
StemAction.CYCLE_NOISE_CONTROL_MODES -> {
|
||||
Log.d("AirPodsParser", "Cycling noise control modes")
|
||||
sendBroadcast(Intent("me.kavishdevar.librepods.SET_ANC_MODE"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processEarDetectionChange(earDetection: ByteArray) {
|
||||
var inEar = false
|
||||
var inEarData = listOf(earDetectionNotification.status[0] == 0x00.toByte(), earDetectionNotification.status[1] == 0x00.toByte())
|
||||
@@ -515,6 +649,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43),
|
||||
textColor = sharedPreferences.getLong("textColor", -1L),
|
||||
qsClickBehavior = sharedPreferences.getString("qs_click_behavior", "cycle") ?: "cycle",
|
||||
bleOnlyMode = sharedPreferences.getBoolean("ble_only_mode", false),
|
||||
|
||||
// AirPods state-based takeover
|
||||
takeoverWhenDisconnected = sharedPreferences.getBoolean("takeover_when_disconnected", true),
|
||||
@@ -524,7 +659,20 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
// Phone state-based takeover
|
||||
takeoverWhenRingingCall = sharedPreferences.getBoolean("takeover_when_ringing_call", true),
|
||||
takeoverWhenMediaStart = sharedPreferences.getBoolean("takeover_when_media_start", true)
|
||||
takeoverWhenMediaStart = sharedPreferences.getBoolean("takeover_when_media_start", true),
|
||||
|
||||
// Stem actions
|
||||
leftSinglePressAction = StemAction.fromString(sharedPreferences.getString("left_single_press_action", "PLAY_PAUSE") ?: "PLAY_PAUSE")!!,
|
||||
rightSinglePressAction = StemAction.fromString(sharedPreferences.getString("right_single_press_action", "PLAY_PAUSE") ?: "PLAY_PAUSE")!!,
|
||||
|
||||
leftDoublePressAction = StemAction.fromString(sharedPreferences.getString("left_double_press_action", "PREVIOUS_TRACK") ?: "NEXT_TRACK")!!,
|
||||
rightDoublePressAction = StemAction.fromString(sharedPreferences.getString("right_double_press_action", "NEXT_TRACK") ?: "NEXT_TRACK")!!,
|
||||
|
||||
leftTriplePressAction = StemAction.fromString(sharedPreferences.getString("left_triple_press_action", "PREVIOUS_TRACK") ?: "PREVIOUS_TRACK")!!,
|
||||
rightTriplePressAction = StemAction.fromString(sharedPreferences.getString("right_triple_press_action", "PREVIOUS_TRACK") ?: "PREVIOUS_TRACK")!!,
|
||||
|
||||
leftLongPressAction = StemAction.fromString(sharedPreferences.getString("left_long_press_action", "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES")!!,
|
||||
rightLongPressAction = StemAction.fromString(sharedPreferences.getString("right_long_press_action", "DIGITAL_ASSISTANT") ?: "DIGITAL_ASSISTANT")!!
|
||||
)
|
||||
}
|
||||
|
||||
@@ -546,6 +694,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
"conversational_awareness_volume" -> config.conversationalAwarenessVolume = preferences.getInt(key, 43)
|
||||
"textColor" -> config.textColor = preferences.getLong(key, -1L)
|
||||
"qs_click_behavior" -> config.qsClickBehavior = preferences.getString(key, "cycle") ?: "cycle"
|
||||
"ble_only_mode" -> config.bleOnlyMode = preferences.getBoolean(key, false)
|
||||
|
||||
// AirPods state-based takeover
|
||||
"takeover_when_disconnected" -> config.takeoverWhenDisconnected = preferences.getBoolean(key, true)
|
||||
@@ -556,6 +705,55 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
// Phone state-based takeover
|
||||
"takeover_when_ringing_call" -> config.takeoverWhenRingingCall = preferences.getBoolean(key, true)
|
||||
"takeover_when_media_start" -> config.takeoverWhenMediaStart = preferences.getBoolean(key, true)
|
||||
|
||||
"left_single_press_action" -> {
|
||||
config.leftSinglePressAction = StemAction.fromString(
|
||||
preferences.getString(key, "PLAY_PAUSE") ?: "PLAY_PAUSE"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"right_single_press_action" -> {
|
||||
config.rightSinglePressAction = StemAction.fromString(
|
||||
preferences.getString(key, "PLAY_PAUSE") ?: "PLAY_PAUSE"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"left_double_press_action" -> {
|
||||
config.leftDoublePressAction = StemAction.fromString(
|
||||
preferences.getString(key, "PREVIOUS_TRACK") ?: "PREVIOUS_TRACK"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"right_double_press_action" -> {
|
||||
config.rightDoublePressAction = StemAction.fromString(
|
||||
preferences.getString(key, "NEXT_TRACK") ?: "NEXT_TRACK"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"left_triple_press_action" -> {
|
||||
config.leftTriplePressAction = StemAction.fromString(
|
||||
preferences.getString(key, "PREVIOUS_TRACK") ?: "PREVIOUS_TRACK"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"right_triple_press_action" -> {
|
||||
config.rightTriplePressAction = StemAction.fromString(
|
||||
preferences.getString(key, "PREVIOUS_TRACK") ?: "PREVIOUS_TRACK"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"left_long_press_action" -> {
|
||||
config.leftLongPressAction = StemAction.fromString(
|
||||
preferences.getString(key, "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
"right_long_press_action" -> {
|
||||
config.rightLongPressAction = StemAction.fromString(
|
||||
preferences.getString(key, "DIGITAL_ASSISTANT") ?: "DIGITAL_ASSISTANT"
|
||||
)!!
|
||||
setupStemActions()
|
||||
}
|
||||
}
|
||||
|
||||
if (key == "mac_address") {
|
||||
@@ -1004,10 +1202,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
if (!::socket.isInitialized) {
|
||||
if (!::socket.isInitialized && !config.bleOnlyMode) {
|
||||
return
|
||||
}
|
||||
if (connected && socket.isConnected) {
|
||||
if (connected && (config.bleOnlyMode || socket.isConnected)) {
|
||||
updatedNotification = NotificationCompat.Builder(this, "airpods_connection_status")
|
||||
.setSmallIcon(R.drawable.airpods)
|
||||
.setContentTitle(airpodsName ?: config.deviceName)
|
||||
@@ -1059,7 +1257,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
notificationManager.notify(1, updatedNotification)
|
||||
notificationManager.cancel(2)
|
||||
} else if (!socket.isConnected && isConnectedLocally) {
|
||||
} else if (!config.bleOnlyMode && !socket.isConnected && isConnectedLocally) {
|
||||
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
|
||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||
}
|
||||
@@ -1070,7 +1268,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
if (isInCall) return
|
||||
if (config.headGestures) {
|
||||
initGestureDetector()
|
||||
aacpManager.sendStartHeadTracking()
|
||||
startHeadTracking()
|
||||
gestureDetector?.startDetection { accepted ->
|
||||
if (accepted) {
|
||||
answerCall()
|
||||
@@ -1376,8 +1574,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
val ancModeFilter = IntentFilter("me.kavishdevar.librepods.SET_ANC_MODE")
|
||||
var ancModeReceiver: BroadcastReceiver? = null
|
||||
|
||||
|
||||
|
||||
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d("AirPodsService", "Service started")
|
||||
@@ -1428,6 +1624,24 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
if (!contains("qs_click_behavior")) editor.putString("qs_click_behavior", "cycle")
|
||||
if (!contains("name")) editor.putString("name", "AirPods")
|
||||
if (!contains("ble_only_mode")) editor.putBoolean("ble_only_mode", false)
|
||||
|
||||
if (!contains("left_single_press_action")) editor.putString("left_single_press_action",
|
||||
StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!.name)
|
||||
if (!contains("right_single_press_action")) editor.putString("right_single_press_action",
|
||||
StemAction.defaultActions[StemPressType.SINGLE_PRESS]!!.name)
|
||||
if (!contains("left_double_press_action")) editor.putString("left_double_press_action",
|
||||
StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!.name)
|
||||
if (!contains("right_double_press_action")) editor.putString("right_double_press_action",
|
||||
StemAction.defaultActions[StemPressType.DOUBLE_PRESS]!!.name)
|
||||
if (!contains("left_triple_press_action")) editor.putString("left_triple_press_action",
|
||||
StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!.name)
|
||||
if (!contains("right_triple_press_action")) editor.putString("right_triple_press_action",
|
||||
StemAction.defaultActions[StemPressType.TRIPLE_PRESS]!!.name)
|
||||
if (!contains("left_long_press_action")) editor.putString("left_long_press_action",
|
||||
StemAction.defaultActions[StemPressType.LONG_PRESS]!!.name)
|
||||
if (!contains("right_long_press_action")) editor.putString("right_long_press_action",
|
||||
StemAction.defaultActions[StemPressType.LONG_PRESS]!!.name)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
@@ -1577,12 +1791,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
|
||||
Log.d("AirPodsCrossDevice", CrossDevice.isAvailable.toString())
|
||||
if (!CrossDevice.isAvailable) {
|
||||
if (!CrossDevice.isAvailable && !config.bleOnlyMode) {
|
||||
Log.d("AirPodsService", "${config.deviceName} connected")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val bluetoothManager = getSystemService(BluetoothManager::class.java)
|
||||
val bluetoothAdapter = bluetoothManager.adapter
|
||||
connectToSocket(bluetoothAdapter, device!!)
|
||||
connectToSocket(device!!)
|
||||
}
|
||||
Log.d("AirPodsService", "Setting metadata")
|
||||
setMetadatas(device!!)
|
||||
@@ -1591,6 +1803,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
sharedPreferences.edit {
|
||||
putString("mac_address", macAddress)
|
||||
}
|
||||
} else if (config.bleOnlyMode) {
|
||||
Log.d("AirPodsService", "BLE-only mode: skipping L2CAP connection")
|
||||
macAddress = device!!.address
|
||||
sharedPreferences.edit {
|
||||
putString("mac_address", macAddress)
|
||||
}
|
||||
}
|
||||
} else if (intent?.action == AirPodsNotifications.AIRPODS_DISCONNECTED) {
|
||||
device = null
|
||||
@@ -1651,15 +1869,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
if (profile == BluetoothProfile.A2DP) {
|
||||
val connectedDevices = proxy.connectedDevices
|
||||
if (connectedDevices.isNotEmpty()) {
|
||||
if (!CrossDevice.isAvailable) {
|
||||
if (!CrossDevice.isAvailable && !config.bleOnlyMode) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
connectToSocket(bluetoothAdapter, device)
|
||||
connectToSocket(device)
|
||||
}
|
||||
setMetadatas(device)
|
||||
macAddress = device.address
|
||||
sharedPreferences.edit {
|
||||
putString("mac_address", macAddress)
|
||||
}
|
||||
} else if (config.bleOnlyMode) {
|
||||
Log.d("AirPodsService", "BLE-only mode: skipping L2CAP connection")
|
||||
macAddress = device.address
|
||||
sharedPreferences.edit {
|
||||
putString("mac_address", macAddress)
|
||||
}
|
||||
}
|
||||
this@AirPodsService.sendBroadcast(
|
||||
Intent(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||
@@ -1759,28 +1983,37 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
Log.d("AirPodsService", macAddress)
|
||||
|
||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) }
|
||||
val bluetoothManager = getSystemService(BluetoothManager::class.java)
|
||||
val bluetoothAdapter = bluetoothManager.adapter
|
||||
device = bluetoothAdapter.bondedDevices.find {
|
||||
device = getSystemService<BluetoothManager>(BluetoothManager::class.java).adapter.bondedDevices.find {
|
||||
it.address == macAddress
|
||||
}
|
||||
|
||||
if (device != null) {
|
||||
connectToSocket(bluetoothAdapter, device!!)
|
||||
connectAudio(this, device)
|
||||
if (config.bleOnlyMode) {
|
||||
// In BLE-only mode, just show connecting status without actual L2CAP connection
|
||||
Log.d("AirPodsService", "BLE-only mode: showing connecting status without L2CAP connection")
|
||||
updateNotificationContent(
|
||||
true,
|
||||
config.deviceName,
|
||||
batteryNotification.getBattery()
|
||||
)
|
||||
// Set a temporary connecting state
|
||||
isConnectedLocally = false // Keep as false since we're not actually connecting to L2CAP
|
||||
} else {
|
||||
connectToSocket(device!!)
|
||||
connectAudio(this, device)
|
||||
isConnectedLocally = true
|
||||
}
|
||||
}
|
||||
|
||||
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!),
|
||||
IslandType.TAKING_OVER)
|
||||
|
||||
isConnectedLocally = true
|
||||
CrossDevice.isAvailable = false
|
||||
}
|
||||
|
||||
private fun createBluetoothSocket(adapter: BluetoothAdapter, device: BluetoothDevice, uuid: ParcelUuid): BluetoothSocket {
|
||||
private fun createBluetoothSocket(device: BluetoothDevice, uuid: ParcelUuid): BluetoothSocket {
|
||||
val type = 3 // L2CAP
|
||||
val constructorSpecs = listOf(
|
||||
arrayOf(adapter, device, type, true, true, 0x1001, uuid),
|
||||
arrayOf(device, type, true, true, 0x1001, uuid),
|
||||
arrayOf(device, type, 1, true, true, 0x1001, uuid),
|
||||
arrayOf(type, 1, true, true, device, 0x1001, uuid),
|
||||
@@ -1817,13 +2050,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||
fun connectToSocket(adapter: BluetoothAdapter, device: BluetoothDevice) {
|
||||
fun connectToSocket(device: BluetoothDevice) {
|
||||
Log.d("AirPodsService", "<LogCollector:Start> Connecting to socket")
|
||||
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||
if (isConnectedLocally != true && !CrossDevice.isAvailable) {
|
||||
socket = try {
|
||||
createBluetoothSocket(adapter, device, uuid)
|
||||
createBluetoothSocket(device, uuid)
|
||||
} catch (e: Exception) {
|
||||
Log.e("AirPodsService", "Failed to create BluetoothSocket: ${e.message}")
|
||||
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.message}")
|
||||
@@ -1886,6 +2119,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
.putExtra("device", device)
|
||||
)
|
||||
|
||||
setupStemActions()
|
||||
|
||||
while (socket.isConnected == true) {
|
||||
socket.let {
|
||||
val buffer = ByteArray(1024)
|
||||
@@ -2138,12 +2373,22 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
fun startHeadTracking() {
|
||||
isHeadTrackingActive = true
|
||||
aacpManager.sendStartHeadTracking()
|
||||
val useAlternatePackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", false)
|
||||
if (useAlternatePackets) {
|
||||
aacpManager.sendDataPacket(aacpManager.createAlternateStartHeadTrackingPacket())
|
||||
} else {
|
||||
aacpManager.sendStartHeadTracking()
|
||||
}
|
||||
HeadTracking.reset()
|
||||
}
|
||||
|
||||
fun stopHeadTracking() {
|
||||
aacpManager.sendStopHeadTracking()
|
||||
val useAlternatePackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", false)
|
||||
if (useAlternatePackets) {
|
||||
aacpManager.sendDataPacket(aacpManager.createAlternateStopHeadTrackingPacket())
|
||||
} else {
|
||||
aacpManager.sendStopHeadTracking()
|
||||
}
|
||||
isHeadTrackingActive = false
|
||||
}
|
||||
|
||||
|
||||
@@ -15,18 +15,21 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.utils
|
||||
|
||||
import android.util.Log
|
||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers.entries
|
||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressBudType.entries
|
||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType.entries
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
/**
|
||||
* Manager class for Apple Accessory Communication Protocol (AACP)
|
||||
* This class is responsible for handling the L2CAP socket management,
|
||||
* constructing and parsing packets for communication with Apple accessories.
|
||||
* constructing and parsing packets for communication with AirPods.
|
||||
*/
|
||||
class AACPManager {
|
||||
companion object {
|
||||
@@ -44,6 +47,7 @@ class AACPManager {
|
||||
const val HEADTRACKING: Byte = 0x17
|
||||
const val PROXIMITY_KEYS_REQ: Byte = 0x30
|
||||
const val PROXIMITY_KEYS_RSP: Byte = 0x31
|
||||
const val STEM_PRESS: Byte = 0x19
|
||||
}
|
||||
|
||||
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
|
||||
@@ -101,8 +105,8 @@ class AACPManager {
|
||||
IN_CASE_TONE_CONFIG(0x31),
|
||||
SIRI_MULTITONE_CONFIG(0x32),
|
||||
HEARING_ASSIST_CONFIG(0x33),
|
||||
ALLOW_OFF_OPTION(0x34);
|
||||
|
||||
ALLOW_OFF_OPTION(0x34),
|
||||
STEM_CONFIG(0x39);
|
||||
companion object {
|
||||
fun fromByte(byte: Byte): ControlCommandIdentifiers? =
|
||||
entries.find { it.value == byte }
|
||||
@@ -118,6 +122,28 @@ class AACPManager {
|
||||
ProximityKeyType.entries.find { it.value == byte }?: throw IllegalArgumentException("Unknown ProximityKeyType: $byte")
|
||||
}
|
||||
}
|
||||
|
||||
enum class StemPressType(val value: Byte) {
|
||||
SINGLE_PRESS(0x05),
|
||||
DOUBLE_PRESS(0x06),
|
||||
TRIPLE_PRESS(0x07),
|
||||
LONG_PRESS(0x08);
|
||||
|
||||
companion object {
|
||||
fun fromByte(byte: Byte): StemPressType? =
|
||||
entries.find { it.value == byte }
|
||||
}
|
||||
}
|
||||
|
||||
enum class StemPressBudType(val value: Byte) {
|
||||
LEFT(0x01),
|
||||
RIGHT(0x02);
|
||||
|
||||
companion object {
|
||||
fun fromByte(byte: Byte): StemPressBudType? =
|
||||
entries.find { it.value == byte }
|
||||
}
|
||||
}
|
||||
}
|
||||
var controlCommandStatusList: MutableList<ControlCommandStatus> = mutableListOf<ControlCommandStatus>()
|
||||
var controlCommandListeners: MutableMap<ControlCommandIdentifiers, MutableList<ControlCommandListener>> = mutableMapOf()
|
||||
@@ -149,6 +175,20 @@ class AACPManager {
|
||||
fun onHeadTrackingReceived(headTracking: ByteArray)
|
||||
fun onUnknownPacketReceived(packet: ByteArray)
|
||||
fun onProximityKeysReceived(proximityKeys: ByteArray)
|
||||
fun onStemPressReceived(stemPress: ByteArray)
|
||||
}
|
||||
|
||||
fun parseStemPressResponse(data: ByteArray): Pair<StemPressType, StemPressBudType> {
|
||||
Log.d(TAG, "Parsing Stem Press Response: ${data.joinToString(" ") { "%02X".format(it) }}")
|
||||
if (data.size != 8) {
|
||||
throw IllegalArgumentException("Data array too short to parse Stem Press Response")
|
||||
}
|
||||
if (data[4] != Opcodes.STEM_PRESS) {
|
||||
throw IllegalArgumentException("Data array does not start with STEM_PRESS opcode")
|
||||
}
|
||||
val type = StemPressType.fromByte(data[6]) ?: throw IllegalArgumentException("Unknown Stem Press Type: ${data[5]}")
|
||||
val bud = StemPressBudType.fromByte(data[7]) ?: throw IllegalArgumentException("Unknown Stem Press Bud Type: ${data[6]}")
|
||||
return Pair(type, bud)
|
||||
}
|
||||
|
||||
interface ControlCommandListener {
|
||||
@@ -195,6 +235,7 @@ class AACPManager {
|
||||
return sendDataPacket(controlPacket)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun sendControlCommand(identifier: Byte, value: Byte): Boolean {
|
||||
val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value))
|
||||
setControlCommandStatusValue(
|
||||
@@ -323,6 +364,9 @@ class AACPManager {
|
||||
Opcodes.PROXIMITY_KEYS_RSP -> {
|
||||
callback?.onProximityKeysReceived(packet)
|
||||
}
|
||||
Opcodes.STEM_PRESS -> {
|
||||
callback?.onStemPressReceived(packet)
|
||||
}
|
||||
else -> {
|
||||
callback?.onUnknownPacketReceived(packet)
|
||||
}
|
||||
@@ -345,7 +389,7 @@ class AACPManager {
|
||||
|
||||
fun createSetFeatureFlagsPacket(): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.SET_FEATURE_FLAGS, 0x00)
|
||||
val data = byteArrayOf(0xFF.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
val data = byteArrayOf(0xD7.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
return opcode + data
|
||||
}
|
||||
|
||||
@@ -370,6 +414,14 @@ class AACPManager {
|
||||
return opcode + data
|
||||
}
|
||||
|
||||
fun createAlternateStartHeadTrackingPacket(): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.HEADTRACKING, 0x00)
|
||||
val data = byteArrayOf(
|
||||
0x00, 0x00, 0x10, 0x00, 0x0F, 0x00, 0x08, 0x73, 0x42, 0x0B, 0x08, 0x10, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x40, 0x9C.toByte(), 0x00, 0x00
|
||||
)
|
||||
return opcode + data
|
||||
}
|
||||
|
||||
fun sendStopHeadTracking(): Boolean {
|
||||
return sendDataPacket(createStopHeadTrackingPacket())
|
||||
}
|
||||
@@ -382,6 +434,14 @@ class AACPManager {
|
||||
return opcode + data
|
||||
}
|
||||
|
||||
fun createAlternateStopHeadTrackingPacket(): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.HEADTRACKING, 0x00)
|
||||
val data = byteArrayOf(
|
||||
0x00, 0x00, 0x10, 0x00, 0x0F, 0x00, 0x08, 0x75, 0x42, 0x0B, 0x08, 0x10, 0x10, 0x02, 0x1A, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00
|
||||
)
|
||||
return opcode + data
|
||||
}
|
||||
|
||||
fun sendRename(name: String): Boolean {
|
||||
return sendDataPacket(createRenamePacket(name))
|
||||
}
|
||||
@@ -440,13 +500,29 @@ class AACPManager {
|
||||
val value = ByteArray(4)
|
||||
System.arraycopy(data, 3, value, 0, 4)
|
||||
|
||||
// drop trailing zeroes in the array, and return the bytearray of the reduced array
|
||||
val trimmedValue = value.takeWhile { it != 0x00.toByte() }.toByteArray()
|
||||
return ControlCommand(identifier, trimmedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun sendStemConfigPacket(
|
||||
singlePressCustomized: Boolean = false,
|
||||
doublePressCustomized: Boolean = false,
|
||||
triplePressCustomized: Boolean = false,
|
||||
longPressCustomized: Boolean = false
|
||||
): Boolean {
|
||||
val value = ((if (singlePressCustomized) 0x01 else 0) or
|
||||
(if (doublePressCustomized) 0x02 else 0) or
|
||||
(if (triplePressCustomized) 0x04 else 0) or
|
||||
(if (longPressCustomized) 0x08 else 0)).toByte()
|
||||
Log.d(TAG, "Sending Stem Config Packet with value: ${value.toHexString()}")
|
||||
return sendControlCommand(
|
||||
ControlCommandIdentifiers.STEM_CONFIG.value, value
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun sendPacket(packet: ByteArray): Boolean {
|
||||
try {
|
||||
|
||||
@@ -58,6 +58,10 @@ import androidx.dynamicanimation.animation.DynamicAnimation
|
||||
import androidx.dynamicanimation.animation.SpringAnimation
|
||||
import androidx.dynamicanimation.animation.SpringForce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.Battery
|
||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.abs
|
||||
@@ -118,6 +122,7 @@ class IslandWindow(private val context: Context) {
|
||||
val isVisible: Boolean
|
||||
get() = containerView.parent != null && containerView.visibility == View.VISIBLE
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun updateBatteryDisplay(batteryList: ArrayList<Battery>?) {
|
||||
if (batteryList == null || batteryList.isEmpty()) return
|
||||
|
||||
@@ -150,7 +155,7 @@ class IslandWindow(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18s", "ClickableViewAccessibility")
|
||||
@SuppressLint("SetTextI18s", "ClickableViewAccessibility", "UnspecifiedRegisterReceiverFlag")
|
||||
fun show(name: String, batteryPercentage: Int, context: Context, type: IslandType = IslandType.CONNECTED) {
|
||||
if (ServiceManager.getService()?.islandOpen == true) return
|
||||
else ServiceManager.getService()?.islandOpen = true
|
||||
@@ -162,13 +167,13 @@ class IslandWindow(private val context: Context) {
|
||||
val batteryList = ServiceManager.getService()?.getBattery()
|
||||
val batteryText = islandView.findViewById<TextView>(R.id.island_battery_text)
|
||||
val batteryProgressBar = islandView.findViewById<ProgressBar>(R.id.island_battery_progress)
|
||||
|
||||
|
||||
val displayBatteryLevel = if (batteryList != null) {
|
||||
val leftBattery = batteryList.find { it.component == BatteryComponent.LEFT }
|
||||
val rightBattery = batteryList.find { it.component == BatteryComponent.RIGHT }
|
||||
|
||||
|
||||
when {
|
||||
leftBattery?.level ?: 0 > 0 && rightBattery?.level ?: 0 > 0 ->
|
||||
leftBattery?.level ?: 0 > 0 && rightBattery?.level ?: 0 > 0 ->
|
||||
minOf(leftBattery!!.level, rightBattery!!.level)
|
||||
leftBattery?.level ?: 0 > 0 -> leftBattery!!.level
|
||||
rightBattery?.level ?: 0 > 0 -> rightBattery!!.level
|
||||
@@ -180,7 +185,7 @@ class IslandWindow(private val context: Context) {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
if (displayBatteryLevel != null) {
|
||||
batteryText.text = "$displayBatteryLevel%"
|
||||
batteryProgressBar.progress = displayBatteryLevel
|
||||
@@ -188,7 +193,7 @@ class IslandWindow(private val context: Context) {
|
||||
batteryText.text = "?"
|
||||
batteryProgressBar.progress = 0
|
||||
}
|
||||
|
||||
|
||||
batteryProgressBar.isIndeterminate = false
|
||||
islandView.findViewById<TextView>(R.id.island_device_name).text = name
|
||||
|
||||
@@ -403,11 +408,11 @@ class IslandWindow(private val context: Context) {
|
||||
|
||||
if (params != null) {
|
||||
params!!.height = screenHeight
|
||||
|
||||
|
||||
val containerParams = containerView.layoutParams
|
||||
containerParams.height = screenHeight
|
||||
containerView.layoutParams = containerParams
|
||||
|
||||
|
||||
try {
|
||||
windowManager.updateViewLayout(containerView, params)
|
||||
} catch (e: Exception) {
|
||||
@@ -552,7 +557,7 @@ class IslandWindow(private val context: Context) {
|
||||
normalizeAnimator.addUpdateListener { animation ->
|
||||
val progress = animation.animatedValue as Float
|
||||
containerView.alpha = progress
|
||||
|
||||
|
||||
if (progress < 0.7f) {
|
||||
islandView.findViewById<VideoView>(R.id.island_video_view).visibility = View.GONE
|
||||
}
|
||||
@@ -620,7 +625,7 @@ class IslandWindow(private val context: Context) {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
|
||||
val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, containerView.scaleX, 0.5f)
|
||||
val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, containerView.scaleY, 0.5f)
|
||||
val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, containerView.translationY, -200f)
|
||||
@@ -640,7 +645,7 @@ class IslandWindow(private val context: Context) {
|
||||
cleanupAndRemoveView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun cleanupAndRemoveView() {
|
||||
containerView.visibility = View.GONE
|
||||
try {
|
||||
@@ -655,25 +660,25 @@ class IslandWindow(private val context: Context) {
|
||||
springAnimation.cancel()
|
||||
flingAnimator.cancel()
|
||||
}
|
||||
|
||||
|
||||
fun forceClose() {
|
||||
try {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(batteryReceiver)
|
||||
} catch (e: Exception) {
|
||||
// Silent catch - receiver might already be unregistered
|
||||
}
|
||||
|
||||
|
||||
ServiceManager.getService()?.islandOpen = false
|
||||
autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return)
|
||||
|
||||
|
||||
// Cancel all ongoing animations
|
||||
springAnimation.cancel()
|
||||
flingAnimator.cancel()
|
||||
|
||||
|
||||
// Immediately remove the view without animations
|
||||
cleanupAndRemoveView()
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -109,67 +109,6 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
|
||||
Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (param.packageName == "com.android.systemui") {
|
||||
Log.i(TAG, "SystemUI detected, hooking volume panel")
|
||||
try {
|
||||
val volumePanelViewClass = param.classLoader.loadClass("com.android.systemui.volume.VolumeDialogImpl")
|
||||
|
||||
try {
|
||||
val initDialogMethod = volumePanelViewClass.getDeclaredMethod("initDialog", Int::class.java)
|
||||
hook(initDialogMethod, VolumeDialogInitHooker::class.java)
|
||||
Log.i(TAG, "Hooked initDialog method successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to hook initDialog method: ${e.message}")
|
||||
}
|
||||
|
||||
try {
|
||||
val showHMethod = volumePanelViewClass.getDeclaredMethod("showH", Int::class.java, Boolean::class.java, Int::class.java)
|
||||
hook(showHMethod, VolumeDialogShowHooker::class.java)
|
||||
Log.i(TAG, "Hooked showH method successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to hook showH method: ${e.message}")
|
||||
}
|
||||
|
||||
Log.i(TAG, "Volume panel hook setup attempted on multiple methods")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to hook volume panel: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@XposedHooker
|
||||
class VolumeDialogInitHooker : XposedInterface.Hooker {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@AfterInvocation
|
||||
fun afterInitDialog(callback: AfterHookCallback) {
|
||||
try {
|
||||
val volumeDialog = callback.thisObject
|
||||
Log.i(TAG, "Volume dialog initialized, adding AirPods controls")
|
||||
addAirPodsControlsToDialog(volumeDialog!!)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in initDialog hook: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@XposedHooker
|
||||
class VolumeDialogShowHooker : XposedInterface.Hooker {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@AfterInvocation
|
||||
fun afterShowH(callback: AfterHookCallback) {
|
||||
try {
|
||||
val volumeDialog = callback.thisObject
|
||||
Log.i(TAG, "Volume dialog shown, ensuring AirPods controls are added")
|
||||
addAirPodsControlsToDialog(volumeDialog!!)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in showH hook: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@XposedHooker
|
||||
|
||||
@@ -100,6 +100,51 @@ object MediaController {
|
||||
return audioManager.isMusicActive
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun sendPlayPause() {
|
||||
if (audioManager.isMusicActive) {
|
||||
Log.d("MediaController", "Sending pause because music is active")
|
||||
sendPause()
|
||||
} else {
|
||||
Log.d("MediaController", "Sending play because music is not active")
|
||||
sendPlay()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun sendPreviousTrack() {
|
||||
Log.d("MediaController", "Sending previous track")
|
||||
audioManager.dispatchMediaKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
)
|
||||
)
|
||||
audioManager.dispatchMediaKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_UP,
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun sendNextTrack() {
|
||||
Log.d("MediaController", "Sending next track")
|
||||
audioManager.dispatchMediaKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
)
|
||||
)
|
||||
audioManager.dispatchMediaKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_UP,
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun sendPause(force: Boolean = false) {
|
||||
Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia, isMusicActive: ${audioManager.isMusicActive}, force: $force")
|
||||
|
||||
@@ -45,6 +45,11 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.VideoView
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.constants.Battery
|
||||
import me.kavishdevar.librepods.constants.BatteryComponent
|
||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||
import kotlin.collections.find
|
||||
|
||||
@SuppressLint("InflateParams", "ClickableViewAccessibility")
|
||||
class PopupWindow(
|
||||
@@ -124,9 +129,9 @@ class PopupWindow(
|
||||
try {
|
||||
if (mView.windowToken == null && mView.parent == null && !isClosing) {
|
||||
mView.findViewById<TextView>(R.id.name).text = name
|
||||
|
||||
|
||||
updateBatteryStatus(batteryNotification)
|
||||
|
||||
|
||||
val vid = mView.findViewById<VideoView>(R.id.video)
|
||||
vid.setVideoPath("android.resource://me.kavishdevar.librepods/" + R.raw.connected)
|
||||
vid.resolveAdjustedSize(vid.width, vid.height)
|
||||
@@ -134,7 +139,7 @@ class PopupWindow(
|
||||
vid.setOnCompletionListener {
|
||||
vid.start()
|
||||
}
|
||||
|
||||
|
||||
mWindowManager.addView(mView, mParams)
|
||||
|
||||
val displayMetrics = mView.context.resources.displayMetrics
|
||||
@@ -144,13 +149,13 @@ class PopupWindow(
|
||||
mView.alpha = 1f
|
||||
|
||||
val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, screenHeight.toFloat(), 0f)
|
||||
|
||||
|
||||
ObjectAnimator.ofPropertyValuesHolder(mView, translationY).apply {
|
||||
duration = 500
|
||||
interpolator = DecelerateInterpolator()
|
||||
start()
|
||||
}
|
||||
|
||||
|
||||
registerBatteryUpdateReceiver()
|
||||
|
||||
autoCloseRunnable = Runnable { close() }
|
||||
@@ -162,6 +167,7 @@ class PopupWindow(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
private fun registerBatteryUpdateReceiver() {
|
||||
batteryUpdateReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -173,7 +179,7 @@ class PopupWindow(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val filter = IntentFilter(AirPodsNotifications.BATTERY_DATA)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(batteryUpdateReceiver, filter, Context.RECEIVER_EXPORTED)
|
||||
@@ -192,7 +198,7 @@ class PopupWindow(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateBatteryStatusFromList(batteryList: List<Battery>) {
|
||||
val batteryLeftText = mView.findViewById<TextView>(R.id.left_battery)
|
||||
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
|
||||
@@ -205,7 +211,7 @@ class PopupWindow(
|
||||
""
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
|
||||
batteryRightText.text = batteryList.find { it.component == BatteryComponent.RIGHT }?.let {
|
||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||
"\uDBC3\uDC8D ${it.level}%"
|
||||
@@ -213,7 +219,7 @@ class PopupWindow(
|
||||
""
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
|
||||
batteryCaseText.text = batteryList.find { it.component == BatteryComponent.CASE }?.let {
|
||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||
"\uDBC3\uDE6C ${it.level}%"
|
||||
@@ -233,13 +239,13 @@ class PopupWindow(
|
||||
try {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
|
||||
|
||||
autoCloseRunnable?.let { autoCloseHandler.removeCallbacks(it) }
|
||||
unregisterBatteryUpdateReceiver()
|
||||
|
||||
|
||||
val vid = mView.findViewById<VideoView>(R.id.video)
|
||||
vid.stopPlayback()
|
||||
|
||||
|
||||
ObjectAnimator.ofFloat(mView, "translationY", mView.height.toFloat()).apply {
|
||||
duration = 500
|
||||
interpolator = AccelerateInterpolator()
|
||||
|
||||
@@ -0,0 +1,616 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.NoLiveLiterals
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@NoLiveLiterals
|
||||
class RadareOffsetFinder(context: Context) {
|
||||
companion object {
|
||||
private const val TAG = "RadareOffsetFinder"
|
||||
private const val RADARE2_URL = "https://hc-cdn.hel1.your-objectstorage.com/s/v3/c9898243c42c0d3d1387de9a37d57ce9df77f9c9_radare2-5.9.9-android-aarch64.tar.gz"
|
||||
private const val HOOK_OFFSET_PROP = "persist.librepods.hook_offset"
|
||||
private const val CFG_REQ_OFFSET_PROP = "persist.librepods.cfg_req_offset"
|
||||
private const val CSM_CONFIG_OFFSET_PROP = "persist.librepods.csm_config_offset"
|
||||
private const val PEER_INFO_REQ_OFFSET_PROP = "persist.librepods.peer_info_req_offset"
|
||||
private const val EXTRACT_DIR = "/"
|
||||
|
||||
private const val RADARE2_BIN_PATH = "$EXTRACT_DIR/data/local/tmp/aln_unzip/org.radare.radare2installer/radare2/bin"
|
||||
private const val RADARE2_LIB_PATH = "$EXTRACT_DIR/data/local/tmp/aln_unzip/org.radare.radare2installer/radare2/lib"
|
||||
private const val BUSYBOX_PATH = "$EXTRACT_DIR/data/local/tmp/aln_unzip/busybox"
|
||||
|
||||
private val LIBRARY_PATHS = listOf(
|
||||
"/apex/com.android.bt/lib64/libbluetooth_jni.so",
|
||||
"/apex/com.android.btservices/lib64/libbluetooth_jni.so",
|
||||
"/system/lib64/libbluetooth_jni.so",
|
||||
"/system/lib64/libbluetooth_qti.so",
|
||||
"/system_ext/lib64/libbluetooth_qti.so"
|
||||
)
|
||||
|
||||
fun findBluetoothLibraryPath(): String? {
|
||||
for (path in LIBRARY_PATHS) {
|
||||
if (File(path).exists()) {
|
||||
Log.d(TAG, "Found Bluetooth library at $path")
|
||||
return path
|
||||
}
|
||||
}
|
||||
Log.e(TAG, "Could not find Bluetooth library")
|
||||
return null
|
||||
}
|
||||
|
||||
fun clearHookOffsets(): Boolean {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec(arrayOf(
|
||||
"su", "-c",
|
||||
"setprop $HOOK_OFFSET_PROP '' && " +
|
||||
"setprop $CFG_REQ_OFFSET_PROP '' && " +
|
||||
"setprop $CSM_CONFIG_OFFSET_PROP '' && " +
|
||||
"setprop $PEER_INFO_REQ_OFFSET_PROP ''"
|
||||
))
|
||||
val exitCode = process.waitFor()
|
||||
|
||||
if (exitCode == 0) {
|
||||
Log.d(TAG, "Successfully cleared hook offset properties")
|
||||
return true
|
||||
} else {
|
||||
Log.e(TAG, "Failed to clear hook offset properties, exit code: $exitCode")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error clearing hook offset properties", e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private val radare2TarballFile = File(context.cacheDir, "radare2.tar.gz")
|
||||
|
||||
private val _progressState = MutableStateFlow<ProgressState>(ProgressState.Idle)
|
||||
val progressState: StateFlow<ProgressState> = _progressState
|
||||
|
||||
sealed class ProgressState {
|
||||
object Idle : ProgressState()
|
||||
object CheckingExisting : ProgressState()
|
||||
object Downloading : ProgressState()
|
||||
data class DownloadProgress(val progress: Float) : ProgressState()
|
||||
object Extracting : ProgressState()
|
||||
object MakingExecutable : ProgressState()
|
||||
object FindingOffset : ProgressState()
|
||||
object SavingOffset : ProgressState()
|
||||
object Cleaning : ProgressState()
|
||||
data class Error(val message: String) : ProgressState()
|
||||
data class Success(val offset: Long) : ProgressState()
|
||||
}
|
||||
|
||||
|
||||
fun isHookOffsetAvailable(): Boolean {
|
||||
Log.d(TAG, "Setup Skipped? " + ServiceManager.getService()?.applicationContext?.getSharedPreferences("settings", Context.MODE_PRIVATE)?.getBoolean("skip_setup", false).toString())
|
||||
if (ServiceManager.getService()?.applicationContext?.getSharedPreferences("settings", Context.MODE_PRIVATE)?.getBoolean("skip_setup", false) == true) {
|
||||
Log.d(TAG, "Setup skipped, returning true.")
|
||||
return true
|
||||
}
|
||||
_progressState.value = ProgressState.CheckingExisting
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec(arrayOf("getprop", HOOK_OFFSET_PROP))
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val propValue = reader.readLine()
|
||||
process.waitFor()
|
||||
|
||||
if (propValue != null && propValue.isNotEmpty()) {
|
||||
Log.d(TAG, "Hook offset property exists: $propValue")
|
||||
_progressState.value = ProgressState.Idle
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking if offset property exists", e)
|
||||
_progressState.value = ProgressState.Error("Failed to check if offset property exists: ${e.message}")
|
||||
}
|
||||
|
||||
Log.d(TAG, "No hook offset available")
|
||||
_progressState.value = ProgressState.Idle
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun setupAndFindOffset(): Boolean {
|
||||
val offset = findOffset()
|
||||
return offset > 0
|
||||
}
|
||||
|
||||
suspend fun findOffset(): Long = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
_progressState.value = ProgressState.Downloading
|
||||
if (!downloadRadare2TarballIfNeeded()) {
|
||||
_progressState.value = ProgressState.Error("Failed to download radare2 tarball")
|
||||
Log.e(TAG, "Failed to download radare2 tarball")
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
_progressState.value = ProgressState.Extracting
|
||||
if (!extractRadare2Tarball()) {
|
||||
_progressState.value = ProgressState.Error("Failed to extract radare2 tarball")
|
||||
Log.e(TAG, "Failed to extract radare2 tarball")
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
_progressState.value = ProgressState.MakingExecutable
|
||||
if (!makeExecutable()) {
|
||||
_progressState.value = ProgressState.Error("Failed to make binaries executable")
|
||||
Log.e(TAG, "Failed to make binaries executable")
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
_progressState.value = ProgressState.FindingOffset
|
||||
val offset = findFunctionOffset()
|
||||
if (offset == 0L) {
|
||||
_progressState.value = ProgressState.Error("Failed to find function offset")
|
||||
Log.e(TAG, "Failed to find function offset")
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
_progressState.value = ProgressState.SavingOffset
|
||||
if (!saveOffset(offset)) {
|
||||
_progressState.value = ProgressState.Error("Failed to save offset")
|
||||
Log.e(TAG, "Failed to save offset")
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
_progressState.value = ProgressState.Cleaning
|
||||
cleanupExtractedFiles()
|
||||
|
||||
_progressState.value = ProgressState.Success(offset)
|
||||
return@withContext offset
|
||||
|
||||
} catch (e: Exception) {
|
||||
_progressState.value = ProgressState.Error("Error: ${e.message}")
|
||||
Log.e(TAG, "Error in findOffset", e)
|
||||
return@withContext 0L
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadRadare2TarballIfNeeded(): Boolean = withContext(Dispatchers.IO) {
|
||||
if (radare2TarballFile.exists() && radare2TarballFile.length() > 0) {
|
||||
Log.d(TAG, "Radare2 tarball already downloaded to ${radare2TarballFile.absolutePath}")
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
try {
|
||||
val url = URL(RADARE2_URL)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 60000
|
||||
connection.readTimeout = 60000
|
||||
|
||||
val contentLength = connection.contentLength.toFloat()
|
||||
val inputStream = connection.inputStream
|
||||
val outputStream = FileOutputStream(radare2TarballFile)
|
||||
|
||||
val buffer = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
var totalBytesRead = 0L
|
||||
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
totalBytesRead += bytesRead
|
||||
if (contentLength > 0) {
|
||||
val progress = totalBytesRead.toFloat() / contentLength
|
||||
_progressState.value = ProgressState.DownloadProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
|
||||
Log.d(TAG, "Download successful to ${radare2TarballFile.absolutePath}")
|
||||
return@withContext true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to download radare2 tarball", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun extractRadare2Tarball(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val isAlreadyExtracted = checkIfAlreadyExtracted()
|
||||
|
||||
if (isAlreadyExtracted) {
|
||||
Log.d(TAG, "Radare2 files already extracted correctly, skipping extraction")
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
Log.d(TAG, "Removing existing extract directory")
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm -rf $EXTRACT_DIR/data/local/tmp/aln_unzip")).waitFor()
|
||||
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p $EXTRACT_DIR/data/local/tmp/aln_unzip")).waitFor()
|
||||
|
||||
Log.d(TAG, "Extracting ${radare2TarballFile.absolutePath} to $EXTRACT_DIR")
|
||||
|
||||
val process = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "tar xvf ${radare2TarballFile.absolutePath} -C $EXTRACT_DIR")
|
||||
)
|
||||
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
|
||||
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "Extract output: $line")
|
||||
}
|
||||
|
||||
while (errorReader.readLine().also { line = it } != null) {
|
||||
Log.e(TAG, "Extract error: $line")
|
||||
}
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode == 0) {
|
||||
Log.d(TAG, "Extraction completed successfully")
|
||||
return@withContext true
|
||||
} else {
|
||||
Log.e(TAG, "Extraction failed with exit code $exitCode")
|
||||
return@withContext false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to extract radare2", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkIfAlreadyExtracted(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val checkDirProcess = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "[ -d $EXTRACT_DIR/data/local/tmp/aln_unzip ] && echo 'exists'")
|
||||
)
|
||||
val dirExists = BufferedReader(InputStreamReader(checkDirProcess.inputStream)).readLine() == "exists"
|
||||
checkDirProcess.waitFor()
|
||||
|
||||
if (!dirExists) {
|
||||
Log.d(TAG, "Extract directory doesn't exist, need to extract")
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
val tarProcess = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "tar tf ${radare2TarballFile.absolutePath}")
|
||||
)
|
||||
val tarFiles = BufferedReader(InputStreamReader(tarProcess.inputStream)).readLines()
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { it.trim() }
|
||||
.toSet()
|
||||
tarProcess.waitFor()
|
||||
|
||||
if (tarFiles.isEmpty()) {
|
||||
Log.e(TAG, "Failed to get file list from tarball")
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
val findProcess = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "find $EXTRACT_DIR/data/local/tmp/aln_unzip -type f | sort")
|
||||
)
|
||||
val extractedFiles = BufferedReader(InputStreamReader(findProcess.inputStream)).readLines()
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { it.trim() }
|
||||
.toSet()
|
||||
findProcess.waitFor()
|
||||
|
||||
if (extractedFiles.isEmpty()) {
|
||||
Log.d(TAG, "No files found in extract directory, need to extract")
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
for (tarFile in tarFiles) {
|
||||
if (tarFile.endsWith("/")) continue
|
||||
|
||||
val filePathInExtractDir = "$EXTRACT_DIR/$tarFile"
|
||||
val fileCheckProcess = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "[ -f $filePathInExtractDir ] && echo 'exists'")
|
||||
)
|
||||
val fileExists = BufferedReader(InputStreamReader(fileCheckProcess.inputStream)).readLine() == "exists"
|
||||
fileCheckProcess.waitFor()
|
||||
|
||||
if (!fileExists) {
|
||||
Log.d(TAG, "File $filePathInExtractDir from tarball missing in extract directory")
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm -rf $EXTRACT_DIR/data/local/tmp/aln_unzip")).waitFor()
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "All ${tarFiles.size} files from tarball exist in extract directory")
|
||||
return@withContext true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking extraction status", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeExecutable(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
Log.d(TAG, "Making binaries executable in $RADARE2_BIN_PATH")
|
||||
val chmod1Result = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "chmod -R 755 $RADARE2_BIN_PATH")
|
||||
).waitFor()
|
||||
|
||||
Log.d(TAG, "Making binaries executable in $BUSYBOX_PATH")
|
||||
|
||||
val chmod2Result = Runtime.getRuntime().exec(
|
||||
arrayOf("su", "-c", "chmod -R 755 $BUSYBOX_PATH")
|
||||
).waitFor()
|
||||
|
||||
if (chmod1Result == 0 && chmod2Result == 0) {
|
||||
Log.d(TAG, "Successfully made binaries executable")
|
||||
return@withContext true
|
||||
} else {
|
||||
Log.e(TAG, "Failed to make binaries executable, exit codes: $chmod1Result, $chmod2Result")
|
||||
return@withContext false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error making binaries executable", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findFunctionOffset(): Long = withContext(Dispatchers.IO) {
|
||||
val libraryPath = findBluetoothLibraryPath() ?: return@withContext 0L
|
||||
var offset = 0L
|
||||
|
||||
try {
|
||||
@Suppress("LocalVariableName") val currentLD_LIBRARY_PATH = ProcessBuilder().command("su", "-c", "printenv LD_LIBRARY_PATH").start().inputStream.bufferedReader().readText().trim()
|
||||
val currentPATH = ProcessBuilder().command("su", "-c", "printenv PATH").start().inputStream.bufferedReader().readText().trim()
|
||||
val envSetup = """
|
||||
export LD_LIBRARY_PATH="$RADARE2_LIB_PATH:$currentLD_LIBRARY_PATH"
|
||||
export PATH="$BUSYBOX_PATH:$RADARE2_BIN_PATH:$currentPATH"
|
||||
""".trimIndent()
|
||||
|
||||
val command = "$envSetup && $RADARE2_BIN_PATH/rabin2 -q -E $libraryPath | grep fcr_chk_chan"
|
||||
Log.d(TAG, "Running command: $command")
|
||||
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
|
||||
|
||||
var line: String?
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 output: $line")
|
||||
if (line?.contains("fcr_chk_chan") == true) {
|
||||
val parts = line.split(" ")
|
||||
if (parts.isNotEmpty() && parts[0].startsWith("0x")) {
|
||||
offset = parts[0].substring(2).toLong(16)
|
||||
Log.d(TAG, "Found offset at ${parts[0]}")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (errorReader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 error: $line")
|
||||
}
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode != 0) {
|
||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||
}
|
||||
|
||||
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
||||
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
||||
// findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to find function offset", e)
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
if (offset == 0L) {
|
||||
Log.e(TAG, "Failed to extract function offset from output, aborting")
|
||||
return@withContext 0L
|
||||
}
|
||||
|
||||
Log.d(TAG, "Successfully found offset: 0x${offset.toString(16)}")
|
||||
return@withContext offset
|
||||
}
|
||||
|
||||
private suspend fun findAndSaveL2cuProcessCfgReqOffset(libraryPath: String, envSetup: String) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val command = "$envSetup && $RADARE2_BIN_PATH/rabin2 -q -E $libraryPath | grep l2cu_process_our_cfg_req"
|
||||
Log.d(TAG, "Running command: $command")
|
||||
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
|
||||
|
||||
var line: String?
|
||||
var offset = 0L
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 output: $line")
|
||||
if (line?.contains("l2cu_process_our_cfg_req") == true) {
|
||||
val parts = line.split(" ")
|
||||
if (parts.isNotEmpty() && parts[0].startsWith("0x")) {
|
||||
offset = parts[0].substring(2).toLong(16)
|
||||
Log.d(TAG, "Found l2cu_process_our_cfg_req offset at ${parts[0]}")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (errorReader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 error: $line")
|
||||
}
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode != 0) {
|
||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||
}
|
||||
|
||||
if (offset > 0L) {
|
||||
val hexString = "0x${offset.toString(16)}"
|
||||
Runtime.getRuntime().exec(arrayOf(
|
||||
"su", "-c", "setprop $CFG_REQ_OFFSET_PROP $hexString"
|
||||
)).waitFor()
|
||||
Log.d(TAG, "Saved l2cu_process_our_cfg_req offset: $hexString")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to find or save l2cu_process_our_cfg_req offset", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findAndSaveL2cCsmConfigOffset(libraryPath: String, envSetup: String) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val command = "$envSetup && $RADARE2_BIN_PATH/rabin2 -q -E $libraryPath | grep l2c_csm_config"
|
||||
Log.d(TAG, "Running command: $command")
|
||||
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
|
||||
|
||||
var line: String?
|
||||
var offset = 0L
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 output: $line")
|
||||
if (line?.contains("l2c_csm_config") == true) {
|
||||
val parts = line.split(" ")
|
||||
if (parts.isNotEmpty() && parts[0].startsWith("0x")) {
|
||||
offset = parts[0].substring(2).toLong(16)
|
||||
Log.d(TAG, "Found l2c_csm_config offset at ${parts[0]}")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (errorReader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 error: $line")
|
||||
}
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode != 0) {
|
||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||
}
|
||||
|
||||
if (offset > 0L) {
|
||||
val hexString = "0x${offset.toString(16)}"
|
||||
Runtime.getRuntime().exec(arrayOf(
|
||||
"su", "-c", "setprop $CSM_CONFIG_OFFSET_PROP $hexString"
|
||||
)).waitFor()
|
||||
Log.d(TAG, "Saved l2c_csm_config offset: $hexString")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to find or save l2c_csm_config offset", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findAndSaveL2cuSendPeerInfoReqOffset(libraryPath: String, envSetup: String) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val command = "$envSetup && $RADARE2_BIN_PATH/rabin2 -q -E $libraryPath | grep l2cu_send_peer_info_req"
|
||||
Log.d(TAG, "Running command: $command")
|
||||
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
|
||||
|
||||
var line: String?
|
||||
var offset = 0L
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 output: $line")
|
||||
if (line?.contains("l2cu_send_peer_info_req") == true) {
|
||||
val parts = line.split(" ")
|
||||
if (parts.isNotEmpty() && parts[0].startsWith("0x")) {
|
||||
offset = parts[0].substring(2).toLong(16)
|
||||
Log.d(TAG, "Found l2cu_send_peer_info_req offset at ${parts[0]}")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (errorReader.readLine().also { line = it } != null) {
|
||||
Log.d(TAG, "rabin2 error: $line")
|
||||
}
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode != 0) {
|
||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||
}
|
||||
|
||||
if (offset > 0L) {
|
||||
val hexString = "0x${offset.toString(16)}"
|
||||
Runtime.getRuntime().exec(arrayOf(
|
||||
"su", "-c", "setprop $PEER_INFO_REQ_OFFSET_PROP $hexString"
|
||||
)).waitFor()
|
||||
Log.d(TAG, "Saved l2cu_send_peer_info_req offset: $hexString")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to find or save l2cu_send_peer_info_req offset", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveOffset(offset: Long): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val hexString = "0x${offset.toString(16)}"
|
||||
Log.d(TAG, "Saving offset to system property: $hexString")
|
||||
|
||||
val process = Runtime.getRuntime().exec(arrayOf(
|
||||
"su", "-c", "setprop $HOOK_OFFSET_PROP $hexString"
|
||||
))
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode == 0) {
|
||||
val verifyProcess = Runtime.getRuntime().exec(arrayOf(
|
||||
"getprop", HOOK_OFFSET_PROP
|
||||
))
|
||||
val propValue = BufferedReader(InputStreamReader(verifyProcess.inputStream)).readLine()
|
||||
verifyProcess.waitFor()
|
||||
|
||||
if (propValue != null && propValue.isNotEmpty()) {
|
||||
Log.d(TAG, "Successfully saved offset to system property: $propValue")
|
||||
return@withContext true
|
||||
} else {
|
||||
Log.e(TAG, "Property was set but couldn't be verified")
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Failed to set property, exit code: $exitCode")
|
||||
}
|
||||
return@withContext false
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save offset", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanupExtractedFiles() {
|
||||
try {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm -rf $EXTRACT_DIR/data/local/tmp/aln_unzip")).waitFor()
|
||||
Log.d(TAG, "Cleaned up extracted files at $EXTRACT_DIR/data/local/tmp/aln_unzip")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to cleanup extracted files", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
10
android/app/src/main/res/drawable/settings_voice.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M320,960Q303,960 291.5,948.5Q280,937 280,920Q280,903 291.5,891.5Q303,880 320,880Q337,880 348.5,891.5Q360,903 360,920Q360,937 348.5,948.5Q337,960 320,960ZM480,960Q463,960 451.5,948.5Q440,937 440,920Q440,903 451.5,891.5Q463,880 480,880Q497,880 508.5,891.5Q520,903 520,920Q520,937 508.5,948.5Q497,960 480,960ZM640,960Q623,960 611.5,948.5Q600,937 600,920Q600,903 611.5,891.5Q623,880 640,880Q657,880 668.5,891.5Q680,903 680,920Q680,937 668.5,948.5Q657,960 640,960ZM480,560Q430,560 395,525Q360,490 360,440L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,490 565,525Q530,560 480,560ZM440,840L440,716Q336,702 268,623.5Q200,545 200,440L280,440Q280,523 338.5,581.5Q397,640 480,640Q563,640 621.5,581.5Q680,523 680,440L760,440Q760,545 692,623.5Q624,702 520,716L520,840L440,840Z"/>
|
||||
</vector>
|
||||
82
android/app/src/main/res/values-zh-rCN/strings.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">LibrePods</string>
|
||||
<string name="app_description" translatable="false">让你的 AirPods 摆脱苹果的生态系统。</string>
|
||||
<string name="title_activity_custom_device" translatable="false">GATT 测试</string>
|
||||
<string name="app_widget_description">在主屏幕上即可查看 AirPods 的电池状态!</string>
|
||||
<string name="accessibility">辅助功能</string>
|
||||
<string name="tone_volume">提示音音量</string>
|
||||
<string name="audio">音频</string>
|
||||
<string name="adaptive_audio">自适应音频</string>
|
||||
<string name="adaptive_audio_description">自适应音频会根据环境动态调整,消除或允许外部噪音。你可以自定义允许的噪音多少。</string>
|
||||
<string name="buds">耳机</string>
|
||||
<string name="case_alt">充电盒</string>
|
||||
<string name="test">测试</string>
|
||||
<string name="name">名称</string>
|
||||
<string name="noise_control">噪音控制</string>
|
||||
<string name="off">关闭</string>
|
||||
<string name="transparency">通透模式</string>
|
||||
<string name="adaptive">自适应</string>
|
||||
<string name="noise_cancellation">主动降噪</string>
|
||||
<string name="press_and_hold_airpods">按住 AirPods</string>
|
||||
<string name="head_gestures">头部手势</string>
|
||||
<string name="left">左耳</string>
|
||||
<string name="right">右耳</string>
|
||||
<string name="adjusts_volume">根据环境调整媒体音量</string>
|
||||
<string name="conversational_awareness">对话感知</string>
|
||||
<string name="conversational_awareness_description">当你开始与他人交谈时,会降低媒体音量并减少背景噪音。</string>
|
||||
<string name="personalized_volume">个性化音量</string>
|
||||
<string name="personalized_volume_description">根据环境自动调整媒体音量。</string>
|
||||
<string name="less_noise">减少噪音</string>
|
||||
<string name="more_noise">增加噪音</string>
|
||||
<string name="noise_cancellation_single_airpod">单只 AirPod 主动降噪</string>
|
||||
<string name="noise_cancellation_single_airpod_description">仅佩戴一只 AirPod 时也能开启主动降噪。</string>
|
||||
<string name="volume_control">音量控制</string>
|
||||
<string name="volume_control_description">通过在 AirPods Pro 柄部传感器上下滑动调节音量。</string>
|
||||
<string name="airpods_not_connected">AirPods 未连接</string>
|
||||
<string name="airpods_not_connected_description">请连接 AirPods 以访问设置。如果卡在此处,请先关闭应用再重新打开。\n(不要强制结束应用!)</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="app_settings">自定义</string>
|
||||
<string name="relative_conversational_awareness_volume">相对音量</string>
|
||||
<string name="relative_conversational_awareness_volume_description">降低到当前音量的百分比,而不是最大音量。</string>
|
||||
<string name="conversational_awareness_pause_music">暂停音乐</string>
|
||||
<string name="conversational_awareness_pause_music_description">当你开始说话时,音乐会自动暂停。</string>
|
||||
<string name="appwidget_text">示例</string>
|
||||
<string name="add_widget">添加小组件</string>
|
||||
<string name="noise_control_widget_description">在主屏幕直接控制噪音模式。</string>
|
||||
<string name="island_connected_text">已连接</string>
|
||||
<string name="island_connected_remote_text">已连接到 Linux</string>
|
||||
<string name="island_taking_over_text">已切换到手机</string>
|
||||
<string name="island_moved_to_remote_text">已切换到 Linux</string>
|
||||
<string name="head_tracking">头部追踪</string>
|
||||
<string name="head_gestures_details">点头接听电话,摇头拒接。</string>
|
||||
<string name="general_settings_header">通用</string>
|
||||
<string name="qs_click_behavior_title">快捷设置磁贴操作</string>
|
||||
<string name="qs_click_behavior_dialog_desc">点击时显示噪音控制对话框。</string>
|
||||
<string name="qs_click_behavior_cycle_desc">点击时循环切换模式。</string>
|
||||
<string name="developer_options_header">开发者</string>
|
||||
<string name="more_settings_title">打开 AirPods 设置</string>
|
||||
<string name="more_settings_subtitle">管理 AirPods 功能与偏好</string>
|
||||
<string name="ear_detection">自动入耳检测</string>
|
||||
<string name="auto_play">自动播放</string>
|
||||
<string name="auto_pause">自动暂停</string>
|
||||
<string name="troubleshooting">故障排查</string>
|
||||
<string name="troubleshooting_description">收集日志以诊断 AirPods 连接问题</string>
|
||||
<string name="collect_logs">收集日志</string>
|
||||
<string name="saved_logs">已保存的日志</string>
|
||||
<string name="no_logs_found">未找到保存的日志</string>
|
||||
<string name="takeover_header">自动连接偏好</string>
|
||||
<string name="takeover_airpods_state">当 AirPods 状态为以下情况时连接:</string>
|
||||
<string name="takeover_disconnected">未连接</string>
|
||||
<string name="takeover_disconnected_desc">AirPods 未连接到任何设备</string>
|
||||
<string name="takeover_idle">空闲</string>
|
||||
<string name="takeover_idle_desc">某设备已连接 AirPods,但未播放媒体或通话</string>
|
||||
<string name="takeover_music">正在播放媒体</string>
|
||||
<string name="takeover_music_desc">某设备正在 AirPods 上播放媒体</string>
|
||||
<string name="takeover_call">正在通话</string>
|
||||
<string name="takeover_call_desc">某设备正在使用 AirPods 通话</string>
|
||||
<string name="takeover_phone_state">当手机处于以下状态时连接 AirPods:</string>
|
||||
<string name="takeover_ringing_call">来电中</string>
|
||||
<string name="takeover_ringing_call_desc">手机开始响铃时</string>
|
||||
<string name="takeover_media_start">开始播放媒体</string>
|
||||
<string name="takeover_media_start_desc">手机开始播放媒体时</string>
|
||||
</resources>
|
||||
BIN
android/imgs/customizations-1.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
android/imgs/customizations-2.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 132 KiB |
@@ -27,7 +27,7 @@ These commands
|
||||
| 0x16 | ClickHoldMode | Two values (2 bytes; First byte = right bud Second byte = for left): `0x01` = Noise control `0x05` = Siri |
|
||||
| 0x17 | DoubleClickInterval | Single value (1 byte): 0x00 = Default, `0x01` = Slower, `0x02` = Slowest|
|
||||
| 0x18 | ClickHoldInterval | Single value (1 byte): 0x00 = Default, `0x01` = Slower, `0x02` = Slowest|
|
||||
| 0x1A | ListeningModeConfigs | Single value (1 byte): bitwise OR of the selected modes. Off mode = `0x01`, ANC=`0x02`, Transparency = 0x04, Adaptive = `0x08` |
|
||||
| 0x1A | ListeningModeConfigs | Single value (1 byte): bitmask, Off mode = `0x01`, ANC=`0x02`, Transparency = 0x04, Adaptive = `0x08` |
|
||||
| 0x1B | OneBudANCMode | Single value (1 byte): `0x01` = enabled, `0x02` = disabled |
|
||||
| 0x1C | CrownRotationDirection | Single value (1 byte): `0x01` = reversed, `0x02` = default |
|
||||
| 0x0D | ListeningMode | Single value (1 byte): 1 = Off, 2 = noise cancellation, 3 = transparency, 4 = adaptive |
|
||||
@@ -48,6 +48,15 @@ These commands
|
||||
| 0x32 | Siri Multitone config | Single value (1 byte) |
|
||||
| 0x33 | Hearing Assist config | Single value (1 byte): `0x01` = enabled, `0x02` = disabled |
|
||||
| 0x34 | Allow Off Option for Listening Mode config | Single value (1 byte): `0x01` = enabled, `0x02` = disabled |
|
||||
| 0x35 | Sleep Detection config | Single value (1 byte): `0x01` = enabled, `0x02` = disabled |
|
||||
| 0x36 | Allow Auto Connect | Single value (1 byte): `0x01` = allow, `0x02` = disallow |
|
||||
| 0x39 | Raw Gestures config | Single value (1 byte): bitmask, single press = `0x01`, double press = `0x02`, triple press = `0x04`, long press = `0x08` |
|
||||
| 0x3C | System Siri message config | Single value (1 byte) |
|
||||
| 0x3E | Uplink EQ Bud config | Single value (1 byte) |
|
||||
| 0x3F | Uplink EQ Source config | Single value (1 byte) |
|
||||
| 0x40 | In Case Tone Volume | Single value (1 byte): 0 to 100 |
|
||||
| 0x41 | Disable Button Input config | Single value (1 byte) |
|
||||
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
BIN
imgs/banner.png
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 199 KiB |
@@ -5,15 +5,15 @@ project(linux VERSION 0.1 LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
qt_standard_project_setup(REQUIRES 6.4)
|
||||
|
||||
qt_add_executable(applinux
|
||||
qt_add_executable(librepods
|
||||
main.cpp
|
||||
main.h
|
||||
logger.h
|
||||
mediacontroller.cpp
|
||||
mediacontroller.h
|
||||
media/mediacontroller.cpp
|
||||
media/mediacontroller.h
|
||||
airpods_packets.h
|
||||
trayiconmanager.cpp
|
||||
trayiconmanager.h
|
||||
@@ -24,9 +24,20 @@ qt_add_executable(applinux
|
||||
autostartmanager.hpp
|
||||
BasicControlCommand.hpp
|
||||
deviceinfo.hpp
|
||||
ble/bleutils.cpp
|
||||
ble/bleutils.h
|
||||
ble/blemanager.cpp
|
||||
ble/blemanager.h
|
||||
thirdparty/QR-Code-generator/qrcodegen.cpp
|
||||
thirdparty/QR-Code-generator/qrcodegen.hpp
|
||||
QRCodeImageProvider.hpp
|
||||
eardetection.hpp
|
||||
media/playerstatuswatcher.cpp
|
||||
media/playerstatuswatcher.h
|
||||
systemsleepmonitor.hpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(applinux
|
||||
qt_add_qml_module(librepods
|
||||
URI linux
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
@@ -35,10 +46,11 @@ qt_add_qml_module(applinux
|
||||
SegmentedControl.qml
|
||||
PodColumn.qml
|
||||
Icon.qml
|
||||
KeysQRDialog.qml
|
||||
)
|
||||
|
||||
# Add the resource file
|
||||
qt_add_resources(applinux "resources"
|
||||
qt_add_resources(librepods "resources"
|
||||
PREFIX "/icons"
|
||||
FILES
|
||||
assets/airpods.png
|
||||
@@ -53,12 +65,12 @@ qt_add_resources(applinux "resources"
|
||||
assets/fonts/SF-Symbols-6.ttf
|
||||
)
|
||||
|
||||
target_link_libraries(applinux
|
||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus
|
||||
target_link_libraries(librepods
|
||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus OpenSSL::SSL OpenSSL::Crypto
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS applinux
|
||||
install(TARGETS librepods
|
||||
BUNDLE DESTINATION .
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
|
||||
69
linux/KeysQRDialog.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Window 2.15
|
||||
|
||||
Window {
|
||||
id: root
|
||||
title: "Magic Cloud Keys QR Code"
|
||||
flags: Qt.Dialog
|
||||
modality: Qt.WindowModal
|
||||
|
||||
// Use system palette for dynamic theming
|
||||
SystemPalette { id: systemPalette }
|
||||
color: systemPalette.window // Background adapts to theme
|
||||
|
||||
width: Math.min(Screen.width * 0.8, 300)
|
||||
height: Math.min(Screen.height * 0.7, 350)
|
||||
|
||||
property string irk: ""
|
||||
property string encKey: ""
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
spacing: 20
|
||||
|
||||
// QR Code Container
|
||||
Rectangle {
|
||||
id: qrContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: width
|
||||
radius: 4
|
||||
color: systemPalette.base
|
||||
border.color: systemPalette.mid
|
||||
|
||||
Image {
|
||||
id: qrCodeImage
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width * 0.9, parent.height * 0.9)
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "image://qrcode/" + root.encKey + ";" + root.irk
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: qrCodeImage.status === Image.Loading
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
visible: qrCodeImage.status === Image.Error
|
||||
text: "Failed to generate QR code"
|
||||
color: systemPalette.text // Dynamic text color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instruction text
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: "Scan this QR code to transfer\nthe Magic Cloud Keys to another device"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
color: systemPalette.text // Adapts to dark/light mode
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ ApplicationWindow {
|
||||
visible: !airPodsTrayApp.hideOnStart
|
||||
width: 400
|
||||
height: 300
|
||||
title: "AirPods Settings"
|
||||
title: "Librepods"
|
||||
objectName: "mainWindowObject"
|
||||
|
||||
onClosing: mainWindow.visible = false
|
||||
@@ -94,7 +94,7 @@ ApplicationWindow {
|
||||
spacing: 8
|
||||
|
||||
PodColumn {
|
||||
isVisible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable
|
||||
visible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable
|
||||
inEar: airPodsTrayApp.deviceInfo.leftPodInEar
|
||||
iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon
|
||||
batteryLevel: airPodsTrayApp.deviceInfo.battery.leftPodLevel
|
||||
@@ -103,7 +103,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
PodColumn {
|
||||
isVisible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable
|
||||
visible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable
|
||||
inEar: airPodsTrayApp.deviceInfo.rightPodInEar
|
||||
iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon
|
||||
batteryLevel: airPodsTrayApp.deviceInfo.battery.rightPodLevel
|
||||
@@ -112,7 +112,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
PodColumn {
|
||||
isVisible: airPodsTrayApp.deviceInfo.battery.caseAvailable
|
||||
visible: airPodsTrayApp.deviceInfo.battery.caseAvailable
|
||||
inEar: true
|
||||
iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.caseIcon
|
||||
batteryLevel: airPodsTrayApp.deviceInfo.battery.caseLevel
|
||||
@@ -164,7 +164,7 @@ ApplicationWindow {
|
||||
anchors.margins: 10
|
||||
font.family: iconFont.name
|
||||
font.pixelSize: 18
|
||||
text: "\uf958" // U+F958
|
||||
text: "\uf958"
|
||||
onClicked: stackView.push(settingsPage)
|
||||
}
|
||||
}
|
||||
@@ -265,9 +265,37 @@ ApplicationWindow {
|
||||
|
||||
Button {
|
||||
text: "Rename"
|
||||
onClicked: airPodsTrayApp.deviceInfo.renameAirPods(newNameField.text)
|
||||
onClicked: airPodsTrayApp.renameAirPods(newNameField.text)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
visible: airPodsTrayApp.airpodsConnected
|
||||
|
||||
TextField {
|
||||
id: newPhoneMacField
|
||||
placeholderText: (PHONE_MAC_ADDRESS !== "" ? PHONE_MAC_ADDRESS : "00:00:00:00:00:00")
|
||||
maximumLength: 32
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Change Phone MAC"
|
||||
onClicked: airPodsTrayApp.setPhoneMac(newPhoneMacField.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
text: "Show Magic Cloud Keys QR"
|
||||
onClicked: keysQrDialog.show()
|
||||
}
|
||||
|
||||
KeysQRDialog {
|
||||
id: keysQrDialog
|
||||
encKey: airPodsTrayApp.deviceInfo.magicAccEncKey
|
||||
irk: airPodsTrayApp.deviceInfo.magicAccIRK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import QtQuick 2.15
|
||||
|
||||
Column {
|
||||
property bool isVisible: true
|
||||
id: root
|
||||
property bool inEar: true
|
||||
property string iconSource
|
||||
property int batteryLevel: 0
|
||||
property bool isCharging: false
|
||||
property string indicator: ""
|
||||
property real targetOpacity: inEar ? 1 : 0.5
|
||||
|
||||
Timer {
|
||||
id: opacityTimer
|
||||
interval: 50
|
||||
onTriggered: root.opacity = root.targetOpacity
|
||||
}
|
||||
|
||||
onInEarChanged: {
|
||||
opacityTimer.restart()
|
||||
}
|
||||
|
||||
spacing: 5
|
||||
opacity: inEar ? 1 : 0.5
|
||||
visible: isVisible
|
||||
|
||||
Image {
|
||||
source: parent.iconSource
|
||||
|
||||
46
linux/QRCodeImageProvider.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include <QQuickImageProvider>
|
||||
#include <QPainter>
|
||||
#include "thirdparty/QR-Code-generator/qrcodegen.hpp"
|
||||
|
||||
class QRCodeImageProvider : public QQuickImageProvider
|
||||
{
|
||||
public:
|
||||
QRCodeImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) {}
|
||||
|
||||
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override
|
||||
{
|
||||
// Parse the keys from id (format: "encKey;irk")
|
||||
QStringList keys = id.split(';');
|
||||
if (keys.size() != 2)
|
||||
return QImage();
|
||||
|
||||
// Create URL format: librepods://add-magic-keys?enc_key=...&irk=...
|
||||
QString data = QString("librepods://add-magic-keys?enc_key=%1&irk=%2").arg(keys[0], keys[1]);
|
||||
|
||||
// Generate QR code using the existing qrcodegen library
|
||||
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::MEDIUM);
|
||||
|
||||
int scale = 8;
|
||||
QImage image(qr.getSize() * scale, qr.getSize() * scale, QImage::Format_RGB32);
|
||||
image.fill(Qt::white);
|
||||
|
||||
QPainter painter(&image);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(Qt::black);
|
||||
|
||||
for (int y = 0; y < qr.getSize(); y++)
|
||||
{
|
||||
for (int x = 0; x < qr.getSize(); x++)
|
||||
{
|
||||
if (qr.getModule(x, y))
|
||||
{
|
||||
painter.drawRect(x * scale, y * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (size)
|
||||
*size = image.size();
|
||||
return image;
|
||||
}
|
||||
};
|
||||
@@ -21,14 +21,29 @@ A native Linux application to control your AirPods, with support for:
|
||||
sudo apt-get install qt6-base-dev qt6-declarative-dev qt6-connectivity-dev qt6-multimedia-dev \
|
||||
qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates \
|
||||
qml6-module-qtquick-window qml6-module-qtquick-layouts
|
||||
```
|
||||
|
||||
# For Fedora
|
||||
sudo dnf install qt6-qtbase-devel qt6-qtconnectivity-devel \
|
||||
qt6-qtmultimedia-devel qt6-qtdeclarative-devel
|
||||
```
|
||||
3. OpenSSL development headers
|
||||
|
||||
```bash
|
||||
# On Arch Linux / EndevaourOS, these are included in the OpenSSL package, so you might already have them installed.
|
||||
sudo pacman -S openssl
|
||||
|
||||
# For Debian / Ubuntu
|
||||
sudo apt-get install libssl-dev
|
||||
|
||||
# For Fedora
|
||||
sudo dnf install openssl-devel
|
||||
```
|
||||
## Setup
|
||||
|
||||
1. Edit `main.h` and update `PHONE_MAC_ADDRESS` with your phone's Bluetooth MAC address:
|
||||
1. Set the `PHONE_MAC_ADDRESS` environment variable to your phone's Bluetooth MAC address by running the following:
|
||||
|
||||
```cpp
|
||||
#define PHONE_MAC_ADDRESS "XX:XX:XX:XX:XX:XX" // Replace with your phone's MAC
|
||||
```bash
|
||||
export PHONE_MAC_ADDRESS="XX:XX:XX:XX:XX:XX" # Replace with your phone's MAC
|
||||
```
|
||||
|
||||
2. Build the application:
|
||||
@@ -43,7 +58,7 @@ A native Linux application to control your AirPods, with support for:
|
||||
3. Run the application:
|
||||
|
||||
```bash
|
||||
./applinux
|
||||
./librepods
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QByteArray>
|
||||
#include <optional>
|
||||
#include <climits>
|
||||
|
||||
#include "enums.h"
|
||||
#include "BasicControlCommand.hpp"
|
||||
@@ -39,13 +40,13 @@ namespace AirPodsPackets
|
||||
|
||||
inline std::optional<NoiseControlMode> parseMode(const QByteArray &data)
|
||||
{
|
||||
char mode = ControlCommand::parseActive(data).value_or(CHAR_MAX);
|
||||
char mode = ControlCommand::parseActive(data).value_or(CHAR_MAX) - 1;
|
||||
if (mode < static_cast<quint8>(NoiseControlMode::MinValue) ||
|
||||
mode > static_cast<quint8>(NoiseControlMode::MaxValue))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<NoiseControlMode>(mode - 1);
|
||||
return static_cast<NoiseControlMode>(mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ namespace AirPodsPackets
|
||||
namespace Connection
|
||||
{
|
||||
static const QByteArray HANDSHAKE = QByteArray::fromHex("00000400010002000000000000000000");
|
||||
static const QByteArray SET_SPECIFIC_FEATURES = QByteArray::fromHex("040004004d00ff00000000000000");
|
||||
static const QByteArray SET_SPECIFIC_FEATURES = QByteArray::fromHex("040004004d00d700000000000000");
|
||||
static const QByteArray REQUEST_NOTIFICATIONS = QByteArray::fromHex("040004000f00ffffffffff");
|
||||
static const QByteArray AIRPODS_DISCONNECTED = QByteArray::fromHex("00010000");
|
||||
}
|
||||
@@ -220,4 +221,4 @@ namespace AirPodsPackets
|
||||
}
|
||||
}
|
||||
|
||||
#endif // AIRPODS_PACKETS_H
|
||||
#endif // AIRPODS_PACKETS_H
|
||||
|
||||
|
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 808 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 484 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -66,7 +66,7 @@ private:
|
||||
"[Desktop Entry]\n"
|
||||
"Type=Application\n"
|
||||
"Name=%1\n"
|
||||
"Exec=%2\n"
|
||||
"Exec=%2 --hide\n"
|
||||
"Icon=%3\n"
|
||||
"Comment=%4\n"
|
||||
"X-GNOME-Autostart-enabled=true\n"
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
#include <climits>
|
||||
|
||||
#include "airpods_packets.h"
|
||||
#include "logger.h"
|
||||
|
||||
class Battery : public QObject
|
||||
{
|
||||
@@ -97,7 +99,10 @@ public:
|
||||
auto level = static_cast<quint8>(packet[offset + 2]);
|
||||
auto status = static_cast<BatteryStatus>(packet[offset + 3]);
|
||||
|
||||
newStates[comp] = {level, status};
|
||||
if (status != BatteryStatus::Disconnected)
|
||||
{
|
||||
newStates[comp] = {level, status};
|
||||
}
|
||||
|
||||
// If this is a pod (Left or Right), add it to the list
|
||||
if (comp == Component::Left || comp == Component::Right)
|
||||
@@ -127,6 +132,61 @@ public:
|
||||
// Emit signal to notify about battery status change
|
||||
emit batteryStatusChanged();
|
||||
|
||||
// Log which is left and right pod
|
||||
LOG_INFO("Primary Pod:" << primaryPod);
|
||||
LOG_INFO("Secondary Pod:" << secondaryPod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseEncryptedPacket(const QByteArray &packet, bool isLeftPodPrimary, bool podInCase)
|
||||
{
|
||||
// Validate packet size (expect 16 bytes based on provided payloads)
|
||||
if (packet.size() != 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine byte indices based on isFlipped
|
||||
int leftByteIndex = isLeftPodPrimary ? 1 : 2;
|
||||
int rightByteIndex = isLeftPodPrimary ? 2 : 1;
|
||||
|
||||
// Extract raw battery bytes
|
||||
unsigned char rawLeftBatteryByte = static_cast<unsigned char>(packet.at(leftByteIndex));
|
||||
unsigned char rawRightBatteryByte = static_cast<unsigned char>(packet.at(rightByteIndex));
|
||||
unsigned char rawCaseBatteryByte = static_cast<unsigned char>(packet.at(3));
|
||||
|
||||
// Extract battery data (charging status and raw level 0-127)
|
||||
auto [isLeftCharging, rawLeftBattery] = formatBattery(rawLeftBatteryByte);
|
||||
auto [isRightCharging, rawRightBattery] = formatBattery(rawRightBatteryByte);
|
||||
auto [isCaseCharging, rawCaseBattery] = formatBattery(rawCaseBatteryByte);
|
||||
|
||||
if (rawLeftBattery == CHAR_MAX) {
|
||||
rawLeftBattery = states.value(Component::Left).level; // Use last valid level
|
||||
isLeftCharging = states.value(Component::Left).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
if (rawRightBattery == CHAR_MAX) {
|
||||
rawRightBattery = states.value(Component::Right).level; // Use last valid level
|
||||
isRightCharging = states.value(Component::Right).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
if (rawCaseBattery == CHAR_MAX) {
|
||||
rawCaseBattery = states.value(Component::Case).level; // Use last valid level
|
||||
isCaseCharging = states.value(Component::Case).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
// Update states
|
||||
states[Component::Left] = {static_cast<quint8>(rawLeftBattery), isLeftCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
states[Component::Right] = {static_cast<quint8>(rawRightBattery), isRightCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
if (podInCase) {
|
||||
states[Component::Case] = {static_cast<quint8>(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
}
|
||||
primaryPod = isLeftPodPrimary ? Component::Left : Component::Right;
|
||||
secondaryPod = isLeftPodPrimary ? Component::Right : Component::Left;
|
||||
emit batteryStatusChanged();
|
||||
emit primaryChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,7 +247,14 @@ private:
|
||||
return states.value(component).status == status;
|
||||
}
|
||||
|
||||
std::pair<bool, int> formatBattery(unsigned char byteVal)
|
||||
{
|
||||
bool charging = (byteVal & 0x80) != 0;
|
||||
int level = byteVal & 0x7F;
|
||||
return std::make_pair(charging, level);
|
||||
}
|
||||
|
||||
QMap<Component, BatteryState> states;
|
||||
Component primaryPod;
|
||||
Component secondaryPod;
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(ble_monitor VERSION 0.1 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Core Bluetooth Widgets)
|
||||
|
||||
qt_add_executable(ble_monitor
|
||||
main.cpp
|
||||
blemanager.h
|
||||
blemanager.cpp
|
||||
blescanner.h
|
||||
blescanner.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(ble_monitor
|
||||
PRIVATE Qt6::Core Qt6::Bluetooth Qt6::Widgets
|
||||
)
|
||||
|
||||
install(TARGETS ble_monitor
|
||||
BUNDLE DESTINATION .
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
@@ -1,6 +1,86 @@
|
||||
#include "blemanager.h"
|
||||
#include "enums.h"
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include "logger.h"
|
||||
#include <QMap>
|
||||
|
||||
AirpodsTrayApp::Enums::AirPodsModel getModelName(quint16 modelId)
|
||||
{
|
||||
using namespace AirpodsTrayApp::Enums;
|
||||
static const QMap<quint16, AirPodsModel> modelMap = {
|
||||
{0x0220, AirPodsModel::AirPods1},
|
||||
{0x0F20, AirPodsModel::AirPods2},
|
||||
{0x1320, AirPodsModel::AirPods3},
|
||||
{0x1920, AirPodsModel::AirPods4},
|
||||
{0x1B20, AirPodsModel::AirPods4ANC},
|
||||
{0x0A20, AirPodsModel::AirPodsMaxLightning},
|
||||
{0x1F20, AirPodsModel::AirPodsMaxUSBC},
|
||||
{0x0E20, AirPodsModel::AirPodsPro},
|
||||
{0x1420, AirPodsModel::AirPodsPro2Lightning},
|
||||
{0x2420, AirPodsModel::AirPodsPro2USBC}
|
||||
};
|
||||
|
||||
return modelMap.value(modelId, AirPodsModel::Unknown);
|
||||
}
|
||||
|
||||
QString getColorName(quint8 colorId)
|
||||
{
|
||||
switch (colorId)
|
||||
{
|
||||
case 0x00:
|
||||
return "White";
|
||||
case 0x01:
|
||||
return "Black";
|
||||
case 0x02:
|
||||
return "Red";
|
||||
case 0x03:
|
||||
return "Blue";
|
||||
case 0x04:
|
||||
return "Pink";
|
||||
case 0x05:
|
||||
return "Gray";
|
||||
case 0x06:
|
||||
return "Silver";
|
||||
case 0x07:
|
||||
return "Gold";
|
||||
case 0x08:
|
||||
return "Rose Gold";
|
||||
case 0x09:
|
||||
return "Space Gray";
|
||||
case 0x0A:
|
||||
return "Dark Blue";
|
||||
case 0x0B:
|
||||
return "Light Blue";
|
||||
case 0x0C:
|
||||
return "Yellow";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
QString getConnectionStateName(BleInfo::ConnectionState state)
|
||||
{
|
||||
using ConnectionState = BleInfo::ConnectionState;
|
||||
switch (state)
|
||||
{
|
||||
case ConnectionState::DISCONNECTED:
|
||||
return QString("Disconnected");
|
||||
case ConnectionState::IDLE:
|
||||
return QString("Idle");
|
||||
case ConnectionState::MUSIC:
|
||||
return QString("Playing Music");
|
||||
case ConnectionState::CALL:
|
||||
return QString("On Call");
|
||||
case ConnectionState::RINGING:
|
||||
return QString("Ringing");
|
||||
case ConnectionState::HANGING_UP:
|
||||
return QString("Hanging Up");
|
||||
case ConnectionState::UNKNOWN:
|
||||
default:
|
||||
return QString("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BleManager::BleManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
@@ -13,36 +93,28 @@ BleManager::BleManager(QObject *parent) : QObject(parent)
|
||||
this, &BleManager::onScanFinished);
|
||||
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred,
|
||||
this, &BleManager::onErrorOccurred);
|
||||
|
||||
// Set up pruning timer
|
||||
pruneTimer = new QTimer(this);
|
||||
connect(pruneTimer, &QTimer::timeout, this, &BleManager::pruneOldDevices);
|
||||
pruneTimer->start(PRUNE_INTERVAL_MS); // Start timer (runs every 5 seconds)
|
||||
}
|
||||
|
||||
BleManager::~BleManager()
|
||||
{
|
||||
delete discoveryAgent;
|
||||
delete pruneTimer;
|
||||
}
|
||||
|
||||
void BleManager::startScan()
|
||||
{
|
||||
qDebug() << "Starting BLE scan...";
|
||||
devices.clear();
|
||||
LOG_DEBUG("Starting BLE scan...");
|
||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
||||
pruneTimer->start(PRUNE_INTERVAL_MS); // Ensure timer is running
|
||||
}
|
||||
|
||||
void BleManager::stopScan()
|
||||
{
|
||||
qDebug() << "Stopping BLE scan...";
|
||||
LOG_DEBUG("Stopping BLE scan...");
|
||||
discoveryAgent->stop();
|
||||
}
|
||||
|
||||
const QMap<QString, DeviceInfo> &BleManager::getDevices() const
|
||||
bool BleManager::isScanning() const
|
||||
{
|
||||
return devices;
|
||||
return discoveryAgent->isActive();
|
||||
}
|
||||
|
||||
void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
@@ -55,10 +127,11 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
if (data.size() >= 10 && data[0] == 0x07)
|
||||
{
|
||||
QString address = info.address().toString();
|
||||
DeviceInfo deviceInfo;
|
||||
BleInfo deviceInfo;
|
||||
deviceInfo.name = info.name().isEmpty() ? "AirPods" : info.name();
|
||||
deviceInfo.address = address;
|
||||
deviceInfo.rawData = data;
|
||||
deviceInfo.rawData = data.left(data.size() - 16);
|
||||
deviceInfo.encryptedPayload = data.mid(data.size() - 16);
|
||||
|
||||
// data[1] is the length of the data, so we can skip it
|
||||
|
||||
@@ -68,8 +141,9 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
return; // Skip pairing mode devices (the values are differently structured)
|
||||
}
|
||||
|
||||
|
||||
// Parse device model (big-endian: high byte at data[3], low byte at data[4])
|
||||
deviceInfo.deviceModel = static_cast<quint16>(data[4]) | (static_cast<quint8>(data[3]) << 8);
|
||||
deviceInfo.modelName = getModelName(static_cast<quint16>(data[4]) | (static_cast<quint8>(data[3]) << 8));
|
||||
|
||||
// Status byte for primary pod and other flags
|
||||
quint8 status = static_cast<quint8>(data[5]);
|
||||
@@ -83,9 +157,9 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
|
||||
// Lid open counter and device color
|
||||
quint8 lidIndicator = static_cast<quint8>(data[8]);
|
||||
deviceInfo.deviceColor = static_cast<quint8>(data[9]);
|
||||
deviceInfo.color = getColorName((quint8)(data[9]));
|
||||
|
||||
deviceInfo.connectionState = static_cast<DeviceInfo::ConnectionState>(data[10]);
|
||||
deviceInfo.connectionState = static_cast<BleInfo::ConnectionState>(data[10]);
|
||||
|
||||
// Next: Encrypted Payload: 16 bytes
|
||||
|
||||
@@ -93,6 +167,8 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
bool primaryLeft = (status & 0x20) != 0; // Bit 5: 1 = left primary, 0 = right primary
|
||||
bool areValuesFlipped = !primaryLeft; // Flipped when right pod is primary
|
||||
|
||||
deviceInfo.primaryLeft = primaryLeft; // Store primary pod information
|
||||
|
||||
// Parse battery levels
|
||||
int leftNibble = areValuesFlipped ? (podsBatteryByte >> 4) & 0x0F : podsBatteryByte & 0x0F;
|
||||
int rightNibble = areValuesFlipped ? podsBatteryByte & 0x0F : (podsBatteryByte >> 4) & 0x0F;
|
||||
@@ -117,6 +193,10 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
deviceInfo.isLeftPodInEar = xorFactor ? (status & 0x08) != 0 : (status & 0x02) != 0; // Bit 3 or 1
|
||||
deviceInfo.isRightPodInEar = xorFactor ? (status & 0x02) != 0 : (status & 0x08) != 0; // Bit 1 or 3
|
||||
|
||||
// Determine primary and secondary in-ear status
|
||||
deviceInfo.isPrimaryInEar = primaryLeft ? deviceInfo.isLeftPodInEar : deviceInfo.isRightPodInEar;
|
||||
deviceInfo.isSecondaryInEar = primaryLeft ? deviceInfo.isRightPodInEar : deviceInfo.isLeftPodInEar;
|
||||
|
||||
// Microphone status
|
||||
deviceInfo.isLeftPodMicrophone = primaryLeft ^ deviceInfo.isThisPodInTheCase;
|
||||
deviceInfo.isRightPodMicrophone = !primaryLeft ^ deviceInfo.isThisPodInTheCase;
|
||||
@@ -124,27 +204,19 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
deviceInfo.lidOpenCounter = lidIndicator & 0x07; // Extract bits 0-2 (count)
|
||||
quint8 lidState = static_cast<quint8>((lidIndicator >> 3) & 0x01); // Extract bit 3 (lid state)
|
||||
if (deviceInfo.isThisPodInTheCase) {
|
||||
deviceInfo.lidState = static_cast<DeviceInfo::LidState>(lidState);
|
||||
deviceInfo.lidState = static_cast<BleInfo::LidState>(lidState);
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
deviceInfo.lastSeen = QDateTime::currentDateTime();
|
||||
|
||||
// Store device info in the map
|
||||
devices[address] = deviceInfo;
|
||||
|
||||
// Debug output
|
||||
qDebug() << "Found device:" << deviceInfo.name
|
||||
<< "Left:" << (deviceInfo.leftPodBattery >= 0 ? QString("%1%").arg(deviceInfo.leftPodBattery) : "N/A")
|
||||
<< "Right:" << (deviceInfo.rightPodBattery >= 0 ? QString("%1%").arg(deviceInfo.rightPodBattery) : "N/A")
|
||||
<< "Case:" << (deviceInfo.caseBattery >= 0 ? QString("%1%").arg(deviceInfo.caseBattery) : "N/A");
|
||||
emit deviceFound(deviceInfo); // Emit signal for device found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BleManager::onScanFinished()
|
||||
{
|
||||
qDebug() << "Scan finished.";
|
||||
if (discoveryAgent->isActive())
|
||||
{
|
||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
||||
@@ -153,24 +225,6 @@ void BleManager::onScanFinished()
|
||||
|
||||
void BleManager::onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error)
|
||||
{
|
||||
qDebug() << "Error occurred:" << error;
|
||||
LOG_ERROR("BLE scan error occurred:" << error);
|
||||
stopScan();
|
||||
}
|
||||
|
||||
void BleManager::pruneOldDevices()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
auto it = devices.begin();
|
||||
while (it != devices.end())
|
||||
{
|
||||
if (it.value().lastSeen.msecsTo(now) > DEVICE_TIMEOUT_MS)
|
||||
{
|
||||
qDebug() << "Removing old device:" << it.value().name << "at" << it.key();
|
||||
it = devices.erase(it); // Remove device if not seen recently
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include "enums.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
class DeviceInfo
|
||||
class BleInfo
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
@@ -20,20 +21,24 @@ public:
|
||||
bool leftCharging = false;
|
||||
bool rightCharging = false;
|
||||
bool caseCharging = false;
|
||||
quint16 deviceModel = 0;
|
||||
AirpodsTrayApp::Enums::AirPodsModel modelName = AirpodsTrayApp::Enums::AirPodsModel::Unknown;
|
||||
quint8 lidOpenCounter = 0;
|
||||
quint8 deviceColor = 0;
|
||||
QString color = "Unknown"; // Default color
|
||||
quint8 status = 0;
|
||||
QByteArray rawData;
|
||||
QByteArray encryptedPayload; // 16 bytes of encrypted payload
|
||||
|
||||
// Additional status flags from Kotlin version
|
||||
bool isLeftPodInEar = false;
|
||||
bool isRightPodInEar = false;
|
||||
bool isPrimaryInEar = false;
|
||||
bool isSecondaryInEar = false;
|
||||
bool isLeftPodMicrophone = false;
|
||||
bool isRightPodMicrophone = false;
|
||||
bool isThisPodInTheCase = false;
|
||||
bool isOnePodInCase = false;
|
||||
bool areBothPodsInCase = false;
|
||||
bool primaryLeft = true; // True if left pod is primary, false if right pod is primary
|
||||
|
||||
// Lid state enumeration
|
||||
enum class LidState
|
||||
@@ -41,8 +46,7 @@ public:
|
||||
OPEN = 0x0,
|
||||
CLOSED = 0x1,
|
||||
UNKNOWN,
|
||||
};
|
||||
LidState lidState = LidState::UNKNOWN;
|
||||
} lidState = LidState::UNKNOWN;
|
||||
|
||||
// Connection state enumeration
|
||||
enum class ConnectionState : uint8_t
|
||||
@@ -54,8 +58,7 @@ public:
|
||||
RINGING = 0x07,
|
||||
HANGING_UP = 0x09,
|
||||
UNKNOWN = 0xFF // Using 0xFF for representing null in the original
|
||||
};
|
||||
ConnectionState connectionState = ConnectionState::UNKNOWN;
|
||||
} connectionState = ConnectionState::UNKNOWN;
|
||||
|
||||
QDateTime lastSeen; // Timestamp of last detection
|
||||
};
|
||||
@@ -69,21 +72,18 @@ public:
|
||||
|
||||
void startScan();
|
||||
void stopScan();
|
||||
const QMap<QString, DeviceInfo> &getDevices() const;
|
||||
bool isScanning() const;
|
||||
|
||||
private slots:
|
||||
void onDeviceDiscovered(const QBluetoothDeviceInfo &info);
|
||||
void onScanFinished();
|
||||
void onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error);
|
||||
void pruneOldDevices();
|
||||
|
||||
signals:
|
||||
void deviceFound(const BleInfo &device);
|
||||
|
||||
private:
|
||||
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||
QMap<QString, DeviceInfo> devices;
|
||||
|
||||
QTimer *pruneTimer; // Timer for periodic pruning
|
||||
static const int PRUNE_INTERVAL_MS = 5000; // Check every 5 seconds
|
||||
static const int DEVICE_TIMEOUT_MS = 10000; // Remove after 10 seconds
|
||||
};
|
||||
|
||||
#endif // BLEMANAGER_H
|
||||
@@ -1,398 +0,0 @@
|
||||
#include "blescanner.h"
|
||||
#include <QApplication>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QProgressBar>
|
||||
#include <QGroupBox>
|
||||
#include <QMenu>
|
||||
|
||||
BleScanner::BleScanner(QWidget *parent) : QMainWindow(parent)
|
||||
{
|
||||
setWindowTitle("AirPods Battery Monitor");
|
||||
resize(600, 400);
|
||||
|
||||
QWidget *centralWidget = new QWidget(this);
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
|
||||
setCentralWidget(centralWidget);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
scanButton = new QPushButton("Start Scan", this);
|
||||
stopButton = new QPushButton("Stop Scan", this);
|
||||
stopButton->setEnabled(false);
|
||||
buttonLayout->addWidget(scanButton);
|
||||
buttonLayout->addWidget(stopButton);
|
||||
buttonLayout->addStretch();
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
deviceTable = new QTableWidget(0, 5, this);
|
||||
deviceTable->setHorizontalHeaderLabels({"Device", "Left Pod", "Right Pod", "Case", "Address"});
|
||||
deviceTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
deviceTable->setSelectionBehavior(QTableWidget::SelectRows);
|
||||
deviceTable->setEditTriggers(QTableWidget::NoEditTriggers);
|
||||
mainLayout->addWidget(deviceTable);
|
||||
|
||||
detailsGroup = new QGroupBox("Device Details", this);
|
||||
QGridLayout *detailsLayout = new QGridLayout(detailsGroup);
|
||||
|
||||
// Row 0: Left Pod
|
||||
detailsLayout->addWidget(new QLabel("Left Pod:"), 0, 0);
|
||||
leftBatteryBar = new QProgressBar(this);
|
||||
leftBatteryBar->setRange(0, 100);
|
||||
leftBatteryBar->setTextVisible(true);
|
||||
detailsLayout->addWidget(leftBatteryBar, 0, 1);
|
||||
leftChargingLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(leftChargingLabel, 0, 2);
|
||||
|
||||
// Row 1: Right Pod
|
||||
detailsLayout->addWidget(new QLabel("Right Pod:"), 1, 0);
|
||||
rightBatteryBar = new QProgressBar(this);
|
||||
rightBatteryBar->setRange(0, 100);
|
||||
rightBatteryBar->setTextVisible(true);
|
||||
detailsLayout->addWidget(rightBatteryBar, 1, 1);
|
||||
rightChargingLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(rightChargingLabel, 1, 2);
|
||||
|
||||
// Row 2: Case
|
||||
detailsLayout->addWidget(new QLabel("Case:"), 2, 0);
|
||||
caseBatteryBar = new QProgressBar(this);
|
||||
caseBatteryBar->setRange(0, 100);
|
||||
caseBatteryBar->setTextVisible(true);
|
||||
detailsLayout->addWidget(caseBatteryBar, 2, 1);
|
||||
caseChargingLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(caseChargingLabel, 2, 2);
|
||||
|
||||
// Row 3: Model
|
||||
detailsLayout->addWidget(new QLabel("Model:"), 3, 0);
|
||||
modelLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(modelLabel, 3, 1);
|
||||
|
||||
// Row 4: Status
|
||||
detailsLayout->addWidget(new QLabel("Status:"), 4, 0);
|
||||
statusLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(statusLabel, 4, 1);
|
||||
|
||||
// Row 5: Lid State (replaces Lid Opens)
|
||||
detailsLayout->addWidget(new QLabel("Lid State:"), 5, 0);
|
||||
lidStateLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(lidStateLabel, 5, 1);
|
||||
|
||||
// Row 6: Color
|
||||
detailsLayout->addWidget(new QLabel("Color:"), 6, 0);
|
||||
colorLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(colorLabel, 6, 1);
|
||||
|
||||
// Row 7: Raw Data
|
||||
detailsLayout->addWidget(new QLabel("Raw Data:"), 7, 0);
|
||||
rawDataLabel = new QLabel(this);
|
||||
rawDataLabel->setWordWrap(true);
|
||||
rawDataLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
detailsLayout->addWidget(rawDataLabel, 7, 1, 1, 2);
|
||||
|
||||
// New Rows for Additional Info
|
||||
// Row 8: Left Pod In Ear
|
||||
detailsLayout->addWidget(new QLabel("Left Pod In Ear:"), 8, 0);
|
||||
leftInEarLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(leftInEarLabel, 8, 1);
|
||||
|
||||
// Row 9: Right Pod In Ear
|
||||
detailsLayout->addWidget(new QLabel("Right Pod In Ear:"), 9, 0);
|
||||
rightInEarLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(rightInEarLabel, 9, 1);
|
||||
|
||||
// Row 10: Left Pod Microphone
|
||||
detailsLayout->addWidget(new QLabel("Left Pod Microphone:"), 10, 0);
|
||||
leftMicLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(leftMicLabel, 10, 1);
|
||||
|
||||
// Row 11: Right Pod Microphone
|
||||
detailsLayout->addWidget(new QLabel("Right Pod Microphone:"), 11, 0);
|
||||
rightMicLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(rightMicLabel, 11, 1);
|
||||
|
||||
// Row 12: This Pod In Case
|
||||
detailsLayout->addWidget(new QLabel("This Pod In Case:"), 12, 0);
|
||||
thisPodInCaseLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(thisPodInCaseLabel, 12, 1);
|
||||
|
||||
// Row 13: One Pod In Case
|
||||
detailsLayout->addWidget(new QLabel("One Pod In Case:"), 13, 0);
|
||||
onePodInCaseLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(onePodInCaseLabel, 13, 1);
|
||||
|
||||
// Row 14: Both Pods In Case
|
||||
detailsLayout->addWidget(new QLabel("Both Pods In Case:"), 14, 0);
|
||||
bothPodsInCaseLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(bothPodsInCaseLabel, 14, 1);
|
||||
|
||||
// Row 15: Connection State
|
||||
detailsLayout->addWidget(new QLabel("Connection State:"), 15, 0);
|
||||
connectionStateLabel = new QLabel(this);
|
||||
detailsLayout->addWidget(connectionStateLabel, 15, 1);
|
||||
|
||||
mainLayout->addWidget(detailsGroup);
|
||||
detailsGroup->setVisible(false);
|
||||
|
||||
bleManager = new BleManager(this);
|
||||
refreshTimer = new QTimer(this);
|
||||
|
||||
connect(scanButton, &QPushButton::clicked, this, &BleScanner::startScan);
|
||||
connect(stopButton, &QPushButton::clicked, this, &BleScanner::stopScan);
|
||||
connect(deviceTable, &QTableWidget::itemSelectionChanged, this, &BleScanner::onDeviceSelected);
|
||||
connect(refreshTimer, &QTimer::timeout, this, &BleScanner::updateDeviceList);
|
||||
}
|
||||
|
||||
void BleScanner::startScan()
|
||||
{
|
||||
scanButton->setEnabled(false);
|
||||
stopButton->setEnabled(true);
|
||||
deviceTable->setRowCount(0);
|
||||
detailsGroup->setVisible(false);
|
||||
bleManager->startScan();
|
||||
refreshTimer->start(500);
|
||||
}
|
||||
|
||||
void BleScanner::stopScan()
|
||||
{
|
||||
bleManager->stopScan();
|
||||
refreshTimer->stop();
|
||||
scanButton->setEnabled(true);
|
||||
stopButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void BleScanner::updateDeviceList()
|
||||
{
|
||||
const QMap<QString, DeviceInfo> &devices = bleManager->getDevices();
|
||||
QString selectedAddress;
|
||||
if (deviceTable->selectionModel()->hasSelection())
|
||||
{
|
||||
int row = deviceTable->selectionModel()->selectedRows().first().row();
|
||||
selectedAddress = deviceTable->item(row, 4)->text();
|
||||
}
|
||||
|
||||
deviceTable->setRowCount(0);
|
||||
deviceTable->setRowCount(devices.size());
|
||||
int row = 0;
|
||||
for (auto it = devices.begin(); it != devices.end(); ++it, ++row)
|
||||
{
|
||||
const DeviceInfo &device = it.value();
|
||||
deviceTable->setItem(row, 0, new QTableWidgetItem(device.name));
|
||||
QString leftStatus = (device.leftPodBattery >= 0 ? QString::number(device.leftPodBattery) + "%" : "N/A") +
|
||||
(device.leftCharging ? " ⚡" : "");
|
||||
deviceTable->setItem(row, 1, new QTableWidgetItem(leftStatus));
|
||||
QString rightStatus = (device.rightPodBattery >= 0 ? QString::number(device.rightPodBattery) + "%" : "N/A") +
|
||||
(device.rightCharging ? " ⚡" : "");
|
||||
deviceTable->setItem(row, 2, new QTableWidgetItem(rightStatus));
|
||||
QString caseStatus = (device.caseBattery >= 0 ? QString::number(device.caseBattery) + "%" : "N/A") +
|
||||
(device.caseCharging ? " ⚡" : "");
|
||||
deviceTable->setItem(row, 3, new QTableWidgetItem(caseStatus));
|
||||
deviceTable->setItem(row, 4, new QTableWidgetItem(device.address));
|
||||
if (device.address == selectedAddress)
|
||||
{
|
||||
deviceTable->selectRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceTable->selectedItems().isEmpty()) {
|
||||
deviceTable->selectRow(0);
|
||||
}
|
||||
}
|
||||
|
||||
void BleScanner::onDeviceSelected()
|
||||
{
|
||||
if (!deviceTable->selectionModel()->hasSelection())
|
||||
{
|
||||
detailsGroup->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int row = deviceTable->selectionModel()->selectedRows().first().row();
|
||||
QString address = deviceTable->item(row, 4)->text();
|
||||
const QMap<QString, DeviceInfo> &devices = bleManager->getDevices();
|
||||
if (!devices.contains(address))
|
||||
{
|
||||
detailsGroup->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const DeviceInfo &device = devices[address];
|
||||
|
||||
// Battery bars with N/A handling
|
||||
if (device.leftPodBattery >= 0)
|
||||
{
|
||||
leftBatteryBar->setValue(device.leftPodBattery);
|
||||
leftBatteryBar->setFormat("%p%");
|
||||
}
|
||||
else
|
||||
{
|
||||
leftBatteryBar->setValue(0);
|
||||
leftBatteryBar->setFormat("N/A");
|
||||
}
|
||||
|
||||
if (device.rightPodBattery >= 0)
|
||||
{
|
||||
rightBatteryBar->setValue(device.rightPodBattery);
|
||||
rightBatteryBar->setFormat("%p%");
|
||||
}
|
||||
else
|
||||
{
|
||||
rightBatteryBar->setValue(0);
|
||||
rightBatteryBar->setFormat("N/A");
|
||||
}
|
||||
|
||||
if (device.caseBattery >= 0)
|
||||
{
|
||||
caseBatteryBar->setValue(device.caseBattery);
|
||||
caseBatteryBar->setFormat("%p%");
|
||||
}
|
||||
else
|
||||
{
|
||||
caseBatteryBar->setValue(0);
|
||||
caseBatteryBar->setFormat("N/A");
|
||||
}
|
||||
|
||||
leftChargingLabel->setText(device.leftCharging ? "Charging" : "Not Charging");
|
||||
rightChargingLabel->setText(device.rightCharging ? "Charging" : "Not Charging");
|
||||
caseChargingLabel->setText(device.caseCharging ? "Charging" : "Not Charging");
|
||||
|
||||
QString modelName = getModelName(device.deviceModel);
|
||||
modelLabel->setText(modelName + " (0x" + QString::number(device.deviceModel, 16).toUpper() + ")");
|
||||
|
||||
QString statusBinary = QString("%1").arg(device.status, 8, 2, QChar('0'));
|
||||
statusLabel->setText(QString("0x%1 (%2) - Binary: %3")
|
||||
.arg(device.status, 2, 16, QChar('0'))
|
||||
.toUpper()
|
||||
.arg(device.status)
|
||||
.arg(statusBinary));
|
||||
|
||||
// Lid State enum handling
|
||||
QString lidStateStr;
|
||||
|
||||
switch (device.lidState)
|
||||
{
|
||||
case DeviceInfo::LidState::OPEN:
|
||||
lidStateStr.append("Open");
|
||||
break;
|
||||
case DeviceInfo::LidState::CLOSED:
|
||||
lidStateStr.append("Closed");
|
||||
break;
|
||||
case DeviceInfo::LidState::UNKNOWN:
|
||||
lidStateStr.append("Unknown");
|
||||
break;
|
||||
}
|
||||
lidStateStr.append(" (0x" + QString::number(device.lidOpenCounter, 16).toUpper() + " = " + QString::number(device.lidOpenCounter) + ")");
|
||||
lidStateLabel->setText(lidStateStr);
|
||||
|
||||
QString colorName = getColorName(device.deviceColor);
|
||||
colorLabel->setText(colorName + " (" + QString::number(device.deviceColor) + ")");
|
||||
|
||||
QString rawDataStr = "Bytes: ";
|
||||
for (int i = 0; i < device.rawData.size(); ++i)
|
||||
{
|
||||
rawDataStr += QString("0x%1 ").arg(static_cast<quint8>(device.rawData[i]), 2, 16, QChar('0')).toUpper();
|
||||
}
|
||||
rawDataLabel->setText(rawDataStr);
|
||||
|
||||
// Set new status labels
|
||||
leftInEarLabel->setText(device.isLeftPodInEar ? "Yes" : "No");
|
||||
rightInEarLabel->setText(device.isRightPodInEar ? "Yes" : "No");
|
||||
leftMicLabel->setText(device.isLeftPodMicrophone ? "Yes" : "No");
|
||||
rightMicLabel->setText(device.isRightPodMicrophone ? "Yes" : "No");
|
||||
thisPodInCaseLabel->setText(device.isThisPodInTheCase ? "Yes" : "No");
|
||||
onePodInCaseLabel->setText(device.isOnePodInCase ? "Yes" : "No");
|
||||
bothPodsInCaseLabel->setText(device.areBothPodsInCase ? "Yes" : "No");
|
||||
connectionStateLabel->setText(getConnectionStateName(device.connectionState));
|
||||
|
||||
detailsGroup->setVisible(true);
|
||||
}
|
||||
|
||||
QString BleScanner::getModelName(quint16 modelId)
|
||||
{
|
||||
switch (modelId)
|
||||
{
|
||||
case 0x0220:
|
||||
return "AirPods 1st Gen";
|
||||
case 0x0F20:
|
||||
return "AirPods 2nd Gen";
|
||||
case 0x1320:
|
||||
return "AirPods 3rd Gen";
|
||||
case 0x1920:
|
||||
return "AirPods 4th Gen";
|
||||
case 0x1B20:
|
||||
return "AirPods 4th Gen (ANC)";
|
||||
case 0x0A20:
|
||||
return "AirPods Max";
|
||||
case 0x1F20:
|
||||
return "AirPods Max (USB-C)";
|
||||
case 0x0E20:
|
||||
return "AirPods Pro";
|
||||
case 0x1420:
|
||||
return "AirPods Pro 2nd Gen";
|
||||
case 0x2420:
|
||||
return "AirPods Pro 2nd Gen (USB-C)";
|
||||
default:
|
||||
return "Unknown Apple Device";
|
||||
}
|
||||
}
|
||||
|
||||
QString BleScanner::getColorName(quint8 colorId)
|
||||
{
|
||||
switch (colorId)
|
||||
{
|
||||
case 0x00:
|
||||
return "White";
|
||||
case 0x01:
|
||||
return "Black";
|
||||
case 0x02:
|
||||
return "Red";
|
||||
case 0x03:
|
||||
return "Blue";
|
||||
case 0x04:
|
||||
return "Pink";
|
||||
case 0x05:
|
||||
return "Gray";
|
||||
case 0x06:
|
||||
return "Silver";
|
||||
case 0x07:
|
||||
return "Gold";
|
||||
case 0x08:
|
||||
return "Rose Gold";
|
||||
case 0x09:
|
||||
return "Space Gray";
|
||||
case 0x0A:
|
||||
return "Dark Blue";
|
||||
case 0x0B:
|
||||
return "Light Blue";
|
||||
case 0x0C:
|
||||
return "Yellow";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
QString BleScanner::getConnectionStateName(DeviceInfo::ConnectionState state)
|
||||
{
|
||||
using ConnectionState = DeviceInfo::ConnectionState;
|
||||
switch (state)
|
||||
{
|
||||
case ConnectionState::DISCONNECTED:
|
||||
return QString("Disconnected");
|
||||
case ConnectionState::IDLE:
|
||||
return QString("Idle");
|
||||
case ConnectionState::MUSIC:
|
||||
return QString("Playing Music");
|
||||
case ConnectionState::CALL:
|
||||
return QString("On Call");
|
||||
case ConnectionState::RINGING:
|
||||
return QString("Ringing");
|
||||
case ConnectionState::HANGING_UP:
|
||||
return QString("Hanging Up");
|
||||
case ConnectionState::UNKNOWN:
|
||||
default:
|
||||
return QString("Unknown");
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#ifndef BLESCANNER_H
|
||||
#define BLESCANNER_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include "blemanager.h"
|
||||
#include <QTimer>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
class QTableWidget;
|
||||
class QGroupBox;
|
||||
class QProgressBar;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class BleScanner : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BleScanner(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void startScan();
|
||||
void stopScan();
|
||||
void updateDeviceList();
|
||||
void onDeviceSelected();
|
||||
|
||||
private:
|
||||
QString getModelName(quint16 modelId);
|
||||
QString getColorName(quint8 colorId);
|
||||
QString getConnectionStateName(DeviceInfo::ConnectionState state);
|
||||
|
||||
BleManager *bleManager;
|
||||
QTimer *refreshTimer;
|
||||
QPushButton *scanButton;
|
||||
QPushButton *stopButton;
|
||||
QTableWidget *deviceTable;
|
||||
QGroupBox *detailsGroup;
|
||||
QProgressBar *leftBatteryBar;
|
||||
QProgressBar *rightBatteryBar;
|
||||
QProgressBar *caseBatteryBar;
|
||||
QLabel *leftChargingLabel;
|
||||
QLabel *rightChargingLabel;
|
||||
QLabel *caseChargingLabel;
|
||||
QLabel *modelLabel;
|
||||
QLabel *statusLabel;
|
||||
QLabel *lidStateLabel; // Renamed from lidOpenLabel
|
||||
QLabel *colorLabel;
|
||||
QLabel *rawDataLabel;
|
||||
|
||||
// New labels for additional DeviceInfo fields
|
||||
QLabel *leftInEarLabel;
|
||||
QLabel *rightInEarLabel;
|
||||
QLabel *leftMicLabel;
|
||||
QLabel *rightMicLabel;
|
||||
QLabel *thisPodInCaseLabel;
|
||||
QLabel *onePodInCaseLabel;
|
||||
QLabel *bothPodsInCaseLabel;
|
||||
QLabel *connectionStateLabel;
|
||||
};
|
||||
|
||||
#endif // BLESCANNER_H
|
||||
138
linux/ble/bleutils.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include <openssl/aes.h>
|
||||
#include "deviceinfo.hpp"
|
||||
#include "bleutils.h"
|
||||
#include <QDebug>
|
||||
#include <QByteArray>
|
||||
#include <QtEndian>
|
||||
#include <QCryptographicHash>
|
||||
#include <cstring> // For memset
|
||||
|
||||
BLEUtils::BLEUtils(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool BLEUtils::verifyRPA(const QString &address, const QByteArray &irk)
|
||||
{
|
||||
if (address.isEmpty() || irk.isEmpty() || irk.size() != 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split address into bytes and reverse order
|
||||
QStringList parts = address.split(':');
|
||||
if (parts.size() != 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray rpa;
|
||||
bool ok;
|
||||
for (int i = parts.size() - 1; i >= 0; --i)
|
||||
{
|
||||
rpa.append(static_cast<char>(parts[i].toInt(&ok, 16)));
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (rpa.size() != 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray prand = rpa.mid(3, 3);
|
||||
QByteArray hash = rpa.left(3);
|
||||
QByteArray computedHash = ah(irk, prand);
|
||||
|
||||
return hash == computedHash;
|
||||
}
|
||||
|
||||
bool BLEUtils::isValidIrkRpa(const QByteArray &irk, const QString &rpa)
|
||||
{
|
||||
return verifyRPA(rpa, irk);
|
||||
}
|
||||
|
||||
QByteArray BLEUtils::e(const QByteArray &key, const QByteArray &data)
|
||||
{
|
||||
if (key.size() != 16 || data.size() != 16)
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// Prepare key and data (needs to be reversed)
|
||||
QByteArray reversedKey(key);
|
||||
std::reverse(reversedKey.begin(), reversedKey.end());
|
||||
|
||||
QByteArray reversedData(data);
|
||||
std::reverse(reversedData.begin(), reversedData.end());
|
||||
|
||||
// Set up AES encryption
|
||||
AES_KEY aesKey;
|
||||
if (AES_set_encrypt_key(reinterpret_cast<const unsigned char *>(reversedKey.constData()), 128, &aesKey) != 0)
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
unsigned char out[16];
|
||||
AES_encrypt(reinterpret_cast<const unsigned char *>(reversedData.constData()), out, &aesKey);
|
||||
|
||||
// Convert output to QByteArray and reverse it
|
||||
QByteArray result(reinterpret_cast<char *>(out), 16);
|
||||
std::reverse(result.begin(), result.end());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray BLEUtils::ah(const QByteArray &k, const QByteArray &r)
|
||||
{
|
||||
if (r.size() < 3)
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// Pad the random part to 16 bytes
|
||||
QByteArray rPadded(16, 0);
|
||||
rPadded.replace(0, 3, r.left(3));
|
||||
|
||||
QByteArray encrypted = e(k, rPadded);
|
||||
if (encrypted.isEmpty())
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return encrypted.left(3);
|
||||
}
|
||||
|
||||
QByteArray BLEUtils::decryptLastBytes(const QByteArray &data, const QByteArray &key)
|
||||
{
|
||||
if (data.size() < 16 || key.size() != 16)
|
||||
{
|
||||
qDebug() << "Invalid input: data size < 16 or key size != 16";
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// Extract the last 16 bytes
|
||||
QByteArray block = data.right(16);
|
||||
|
||||
// Set up AES decryption key (use key directly, no reversal)
|
||||
AES_KEY aesKey;
|
||||
if (AES_set_decrypt_key(reinterpret_cast<const unsigned char *>(key.constData()), 128, &aesKey) != 0)
|
||||
{
|
||||
qDebug() << "Failed to set AES decryption key";
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
unsigned char out[16];
|
||||
unsigned char iv[16];
|
||||
memset(iv, 0, 16); // Zero IV for CBC mode
|
||||
|
||||
// Perform AES decryption using CBC mode with zero IV
|
||||
// AES_cbc_encrypt is used for both encryption and decryption depending on the key schedule
|
||||
AES_cbc_encrypt(reinterpret_cast<const unsigned char *>(block.constData()), out, 16, &aesKey, iv, AES_DECRYPT);
|
||||
|
||||
// Convert output to QByteArray (no reversal)
|
||||
QByteArray result(reinterpret_cast<char *>(out), 16);
|
||||
|
||||
return result;
|
||||
}
|
||||
52
linux/ble/bleutils.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
|
||||
class BLEUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BLEUtils(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Verifies if the provided Bluetooth address is an RPA that matches the given Identity Resolving Key (IRK)
|
||||
* @param address The Bluetooth address to verify
|
||||
* @param irk The Identity Resolving Key to use for verification
|
||||
* @return true if the address is verified as an RPA matching the IRK
|
||||
*/
|
||||
static bool verifyRPA(const QString &address, const QByteArray &irk);
|
||||
|
||||
/**
|
||||
* @brief Checks if the given IRK and RPA are valid
|
||||
* @param irk The Identity Resolving Key
|
||||
* @param rpa The Resolvable Private Address
|
||||
* @return true if the RPA is valid for the given IRK
|
||||
*/
|
||||
Q_INVOKABLE static bool isValidIrkRpa(const QByteArray &irk, const QString &rpa);
|
||||
|
||||
/**
|
||||
* @brief Decrypts the last 16 bytes of the input data using the provided key with AES-128 ECB
|
||||
* @param data The input data containing at least 16 bytes
|
||||
* @param key The 16-byte key for decryption
|
||||
* @return The decrypted 16 bytes, or an empty QByteArray on failure
|
||||
*/
|
||||
static QByteArray decryptLastBytes(const QByteArray &data, const QByteArray &key);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Performs E function (AES-128) as specified in Bluetooth Core Specification
|
||||
* @param key The key for encryption
|
||||
* @param data The data to encrypt
|
||||
* @return The encrypted data
|
||||
*/
|
||||
static QByteArray e(const QByteArray &key, const QByteArray &data);
|
||||
|
||||
/**
|
||||
* @brief Performs the ah function as specified in Bluetooth Core Specification
|
||||
* @param k The IRK key
|
||||
* @param r The random part of the address
|
||||
* @return The hash part of the address
|
||||
*/
|
||||
static QByteArray ah(const QByteArray &k, const QByteArray &r);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "blescanner.h"
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
BleScanner scanner;
|
||||
scanner.show();
|
||||
return app.exec();
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QSettings>
|
||||
#include "battery.hpp"
|
||||
#include "enums.h"
|
||||
#include "eardetection.hpp"
|
||||
|
||||
using namespace AirpodsTrayApp::Enums;
|
||||
|
||||
@@ -12,14 +13,11 @@ class DeviceInfo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString batteryStatus READ batteryStatus WRITE setBatteryStatus NOTIFY batteryStatusChanged)
|
||||
Q_PROPERTY(QString earDetectionStatus READ earDetectionStatus WRITE setEarDetectionStatus NOTIFY earDetectionStatusChanged)
|
||||
Q_PROPERTY(int noiseControlMode READ noiseControlModeInt WRITE setNoiseControlModeInt NOTIFY noiseControlModeChangedInt)
|
||||
Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged)
|
||||
Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged)
|
||||
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
|
||||
Q_PROPERTY(Battery *battery READ getBattery CONSTANT)
|
||||
Q_PROPERTY(bool primaryInEar READ isPrimaryInEar WRITE setPrimaryInEar NOTIFY primaryChanged)
|
||||
Q_PROPERTY(bool secondaryInEar READ isSecondaryInEar WRITE setSecondaryInEar NOTIFY primaryChanged)
|
||||
Q_PROPERTY(bool oneBudANCMode READ oneBudANCMode WRITE setOneBudANCMode NOTIFY oneBudANCModeChanged)
|
||||
Q_PROPERTY(AirPodsModel model READ model WRITE setModel NOTIFY modelChanged)
|
||||
Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChangedInt)
|
||||
@@ -28,9 +26,13 @@ class DeviceInfo : public QObject
|
||||
Q_PROPERTY(bool leftPodInEar READ isLeftPodInEar NOTIFY primaryChanged)
|
||||
Q_PROPERTY(bool rightPodInEar READ isRightPodInEar NOTIFY primaryChanged)
|
||||
Q_PROPERTY(QString bluetoothAddress READ bluetoothAddress WRITE setBluetoothAddress NOTIFY bluetoothAddressChanged)
|
||||
Q_PROPERTY(QString magicAccIRK READ magicAccIRKHex CONSTANT)
|
||||
Q_PROPERTY(QString magicAccEncKey READ magicAccEncKeyHex CONSTANT)
|
||||
|
||||
public:
|
||||
explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)) {}
|
||||
explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)), m_earDetection(new EarDetection(this)) {
|
||||
connect(getEarDetection(), &EarDetection::statusChanged, this, &DeviceInfo::primaryChanged);
|
||||
}
|
||||
|
||||
QString batteryStatus() const { return m_batteryStatus; }
|
||||
void setBatteryStatus(const QString &status)
|
||||
@@ -42,16 +44,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
QString earDetectionStatus() const { return m_earDetectionStatus; }
|
||||
void setEarDetectionStatus(const QString &status)
|
||||
{
|
||||
if (m_earDetectionStatus != status)
|
||||
{
|
||||
m_earDetectionStatus = status;
|
||||
emit earDetectionStatusChanged(status);
|
||||
}
|
||||
}
|
||||
|
||||
NoiseControlMode noiseControlMode() const { return m_noiseControlMode; }
|
||||
void setNoiseControlMode(NoiseControlMode mode)
|
||||
{
|
||||
@@ -97,26 +89,6 @@ public:
|
||||
|
||||
Battery *getBattery() const { return m_battery; }
|
||||
|
||||
bool isPrimaryInEar() const { return m_primaryInEar; }
|
||||
void setPrimaryInEar(bool inEar)
|
||||
{
|
||||
if (m_primaryInEar != inEar)
|
||||
{
|
||||
m_primaryInEar = inEar;
|
||||
emit primaryChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool isSecondaryInEar() const { return m_secoundaryInEar; }
|
||||
void setSecondaryInEar(bool inEar)
|
||||
{
|
||||
if (m_secoundaryInEar != inEar)
|
||||
{
|
||||
m_secoundaryInEar = inEar;
|
||||
emit primaryChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool oneBudANCMode() const { return m_oneBudANCMode; }
|
||||
void setOneBudANCMode(bool enabled)
|
||||
{
|
||||
@@ -139,9 +111,11 @@ public:
|
||||
|
||||
QByteArray magicAccIRK() const { return m_magicAccIRK; }
|
||||
void setMagicAccIRK(const QByteArray &irk) { m_magicAccIRK = irk; }
|
||||
QString magicAccIRKHex() const { return QString::fromUtf8(m_magicAccIRK.toHex()); }
|
||||
|
||||
QByteArray magicAccEncKey() const { return m_magicAccEncKey; }
|
||||
void setMagicAccEncKey(const QByteArray &key) { m_magicAccEncKey = key; }
|
||||
QString magicAccEncKeyHex() const { return QString::fromUtf8(m_magicAccEncKey.toHex()); }
|
||||
|
||||
QString modelNumber() const { return m_modelNumber; }
|
||||
void setModelNumber(const QString &modelNumber) { m_modelNumber = modelNumber; }
|
||||
@@ -163,18 +137,18 @@ public:
|
||||
QString caseIcon() const { return getModelIcon(model()).second; }
|
||||
bool isLeftPodInEar() const
|
||||
{
|
||||
if (getBattery()->getPrimaryPod() == Battery::Component::Left) return isPrimaryInEar();
|
||||
else return isSecondaryInEar();
|
||||
if (getBattery()->getPrimaryPod() == Battery::Component::Left) return getEarDetection()->isPrimaryInEar();
|
||||
else return getEarDetection()->isSecondaryInEar();
|
||||
}
|
||||
bool isRightPodInEar() const
|
||||
{
|
||||
if (getBattery()->getPrimaryPod() == Battery::Component::Right) return isPrimaryInEar();
|
||||
else return isSecondaryInEar();
|
||||
if (getBattery()->getPrimaryPod() == Battery::Component::Right) return getEarDetection()->isPrimaryInEar();
|
||||
else return getEarDetection()->isSecondaryInEar();
|
||||
}
|
||||
|
||||
bool adaptiveModeActive() const { return noiseControlMode() == NoiseControlMode::Adaptive; }
|
||||
bool oneOrMorePodsInCase() const { return earDetectionStatus().contains("In case"); }
|
||||
bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); }
|
||||
|
||||
EarDetection *getEarDetection() const { return m_earDetection; }
|
||||
|
||||
void reset()
|
||||
{
|
||||
@@ -182,38 +156,38 @@ public:
|
||||
setModel(AirPodsModel::Unknown);
|
||||
m_battery->reset();
|
||||
setBatteryStatus("");
|
||||
setEarDetectionStatus("");
|
||||
setPrimaryInEar(false);
|
||||
setSecondaryInEar(false);
|
||||
setNoiseControlMode(NoiseControlMode::Off);
|
||||
setBluetoothAddress("");
|
||||
getEarDetection()->reset();
|
||||
}
|
||||
|
||||
void save() const
|
||||
void saveToSettings(QSettings &settings)
|
||||
{
|
||||
QSettings settings("AirpodsTrayApp", "DeviceInfo");
|
||||
settings.beginGroup("DeviceInfo");
|
||||
settings.setValue("deviceName", m_deviceName);
|
||||
settings.setValue("bluetoothAddress", m_bluetoothAddress);
|
||||
settings.setValue("magicAccIRK", m_magicAccIRK.toBase64());
|
||||
settings.setValue("magicAccEncKey", m_magicAccEncKey.toBase64());
|
||||
settings.setValue("deviceName", deviceName());
|
||||
settings.setValue("model", static_cast<int>(model()));
|
||||
settings.setValue("magicAccIRK", magicAccIRK());
|
||||
settings.setValue("magicAccEncKey", magicAccEncKey());
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
void load()
|
||||
void loadFromSettings(const QSettings &settings)
|
||||
{
|
||||
QSettings settings("AirpodsTrayApp", "DeviceInfo");
|
||||
settings.beginGroup("DeviceInfo");
|
||||
setDeviceName(settings.value("deviceName", "").toString());
|
||||
setBluetoothAddress(settings.value("bluetoothAddress", "").toString());
|
||||
setMagicAccIRK(QByteArray::fromBase64(settings.value("magicAccIRK", "").toByteArray()));
|
||||
setMagicAccEncKey(QByteArray::fromBase64(settings.value("magicAccEncKey", "").toByteArray()));
|
||||
settings.endGroup();
|
||||
setDeviceName(settings.value("DeviceInfo/deviceName", "").toString());
|
||||
setModel(static_cast<AirPodsModel>(settings.value("DeviceInfo/model", (int)(AirPodsModel::Unknown)).toInt()));
|
||||
setMagicAccIRK(settings.value("DeviceInfo/magicAccIRK", QByteArray()).toByteArray());
|
||||
setMagicAccEncKey(settings.value("DeviceInfo/magicAccEncKey", QByteArray()).toByteArray());
|
||||
}
|
||||
|
||||
void updateBatteryStatus()
|
||||
{
|
||||
int leftLevel = getBattery()->getState(Battery::Component::Left).level;
|
||||
int rightLevel = getBattery()->getState(Battery::Component::Right).level;
|
||||
int caseLevel = getBattery()->getState(Battery::Component::Case).level;
|
||||
setBatteryStatus(QString("Left: %1%, Right: %2%, Case: %3%").arg(leftLevel).arg(rightLevel).arg(caseLevel));
|
||||
}
|
||||
|
||||
signals:
|
||||
void batteryStatusChanged(const QString &status);
|
||||
void earDetectionStatusChanged(const QString &status);
|
||||
void noiseControlModeChanged(NoiseControlMode mode);
|
||||
void noiseControlModeChangedInt(int mode);
|
||||
void conversationalAwarenessChanged(bool enabled);
|
||||
@@ -226,14 +200,11 @@ signals:
|
||||
|
||||
private:
|
||||
QString m_batteryStatus;
|
||||
QString m_earDetectionStatus;
|
||||
NoiseControlMode m_noiseControlMode = NoiseControlMode::Off;
|
||||
NoiseControlMode m_noiseControlMode = NoiseControlMode::Transparency;
|
||||
bool m_conversationalAwareness = false;
|
||||
int m_adaptiveNoiseLevel = 50;
|
||||
QString m_deviceName;
|
||||
Battery *m_battery;
|
||||
bool m_primaryInEar = false;
|
||||
bool m_secoundaryInEar = false;
|
||||
QByteArray m_magicAccIRK;
|
||||
QByteArray m_magicAccEncKey;
|
||||
bool m_oneBudANCMode = false;
|
||||
@@ -241,4 +212,5 @@ private:
|
||||
QString m_modelNumber;
|
||||
QString m_manufacturer;
|
||||
QString m_bluetoothAddress;
|
||||
EarDetection *m_earDetection;
|
||||
};
|
||||
94
linux/eardetection.hpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include "logger.h"
|
||||
|
||||
class EarDetection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class EarDetectionStatus
|
||||
{
|
||||
InEar,
|
||||
NotInEar,
|
||||
InCase,
|
||||
Disconnected,
|
||||
};
|
||||
Q_ENUM(EarDetectionStatus)
|
||||
|
||||
explicit EarDetection(QObject *parent = nullptr) : QObject(parent)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
primaryStatus = EarDetectionStatus::Disconnected;
|
||||
secondaryStatus = EarDetectionStatus::Disconnected;
|
||||
emit statusChanged();
|
||||
}
|
||||
|
||||
bool parseData(const QByteArray &data)
|
||||
{
|
||||
if (data.size() < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto [newprimaryStatus, newsecondaryStatus] = parseStatusBytes(data);
|
||||
|
||||
primaryStatus = newprimaryStatus;
|
||||
secondaryStatus = newsecondaryStatus;
|
||||
LOG_DEBUG("Parsed Ear Detection Status: Primary - " << primaryStatus
|
||||
<< ", Secondary - " << secondaryStatus);
|
||||
emit statusChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
void overrideEarDetectionStatus(bool primaryInEar, bool secondaryInEar)
|
||||
{
|
||||
primaryStatus = primaryInEar ? EarDetectionStatus::InEar : EarDetectionStatus::NotInEar;
|
||||
secondaryStatus = secondaryInEar ? EarDetectionStatus::InEar : EarDetectionStatus::NotInEar;
|
||||
emit statusChanged();
|
||||
}
|
||||
|
||||
bool isPrimaryInEar() const { return primaryStatus == EarDetectionStatus::InEar; }
|
||||
bool isSecondaryInEar() const { return secondaryStatus == EarDetectionStatus::InEar; }
|
||||
bool oneOrMorePodsInCase() const { return primaryStatus == EarDetectionStatus::InCase || secondaryStatus == EarDetectionStatus::InCase; }
|
||||
bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); }
|
||||
|
||||
EarDetectionStatus getprimaryStatus() const { return primaryStatus; }
|
||||
EarDetectionStatus getsecondaryStatus() const { return secondaryStatus; }
|
||||
|
||||
signals:
|
||||
void statusChanged();
|
||||
|
||||
private:
|
||||
QPair<EarDetectionStatus, EarDetectionStatus> parseStatusBytes(const QByteArray &data) const
|
||||
{
|
||||
quint8 primaryByte = static_cast<quint8>(data[6]);
|
||||
quint8 secondaryByte = static_cast<quint8>(data[7]);
|
||||
|
||||
auto primaryStatus = parseStatusByte(primaryByte);
|
||||
auto secondaryStatus = parseStatusByte(secondaryByte);
|
||||
|
||||
return qMakePair(primaryStatus, secondaryStatus);
|
||||
}
|
||||
|
||||
EarDetectionStatus parseStatusByte(quint8 byte) const
|
||||
{
|
||||
if (byte == 0x00)
|
||||
return EarDetectionStatus::InEar;
|
||||
if (byte == 0x01)
|
||||
return EarDetectionStatus::NotInEar;
|
||||
if (byte == 0x02)
|
||||
return EarDetectionStatus::InCase;
|
||||
return EarDetectionStatus::Disconnected;
|
||||
}
|
||||
|
||||
EarDetectionStatus primaryStatus = EarDetectionStatus::Disconnected;
|
||||
EarDetectionStatus secondaryStatus = EarDetectionStatus::Disconnected;
|
||||
};
|
||||
@@ -3,9 +3,9 @@
|
||||
#include <QDebug>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(airpodsApp)
|
||||
Q_DECLARE_LOGGING_CATEGORY(Librepods)
|
||||
|
||||
#define LOG_INFO(msg) qCInfo(airpodsApp) << "\033[32m" << msg << "\033[0m"
|
||||
#define LOG_WARN(msg) qCWarning(airpodsApp) << "\033[33m" << msg << "\033[0m"
|
||||
#define LOG_ERROR(msg) qCCritical(airpodsApp) << "\033[31m" << msg << "\033[0m"
|
||||
#define LOG_DEBUG(msg) qCDebug(airpodsApp) << "\033[34m" << msg << "\033[0m"
|
||||
#define LOG_INFO(msg) qCInfo(Librepods) << "\033[32m" << msg << "\033[0m"
|
||||
#define LOG_WARN(msg) qCWarning(Librepods) << "\033[33m" << msg << "\033[0m"
|
||||
#define LOG_ERROR(msg) qCCritical(Librepods) << "\033[31m" << msg << "\033[0m"
|
||||
#define LOG_DEBUG(msg) qCDebug(Librepods) << "\033[34m" << msg << "\033[0m"
|
||||
|
||||
191
linux/main.cpp
@@ -1,20 +1,35 @@
|
||||
#include <QSettings>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include "main.h"
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QBluetoothSocket>
|
||||
#include <QQuickWindow>
|
||||
#include <QLoggingCategory>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "airpods_packets.h"
|
||||
#include "logger.h"
|
||||
#include "mediacontroller.h"
|
||||
#include "media/mediacontroller.h"
|
||||
#include "trayiconmanager.h"
|
||||
#include "enums.h"
|
||||
#include "battery.hpp"
|
||||
#include "BluetoothMonitor.h"
|
||||
#include "autostartmanager.hpp"
|
||||
#include "deviceinfo.hpp"
|
||||
#include "ble/blemanager.h"
|
||||
#include "ble/bleutils.h"
|
||||
#include "QRCodeImageProvider.hpp"
|
||||
#include "systemsleepmonitor.hpp"
|
||||
|
||||
using namespace AirpodsTrayApp::Enums;
|
||||
|
||||
Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp")
|
||||
Q_LOGGING_CATEGORY(Librepods, "Librepods")
|
||||
|
||||
class AirPodsTrayApp : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -26,12 +41,16 @@ class AirPodsTrayApp : public QObject {
|
||||
Q_PROPERTY(int retryAttempts READ retryAttempts WRITE setRetryAttempts NOTIFY retryAttemptsChanged)
|
||||
Q_PROPERTY(bool hideOnStart READ hideOnStart CONSTANT)
|
||||
Q_PROPERTY(DeviceInfo *deviceInfo READ deviceInfo CONSTANT)
|
||||
Q_PROPERTY(QString phoneMacStatus READ phoneMacStatus NOTIFY phoneMacStatusChanged)
|
||||
|
||||
public:
|
||||
AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr)
|
||||
: QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")), m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent), m_deviceInfo(new DeviceInfo(this))
|
||||
: QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp"))
|
||||
, m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent)
|
||||
, m_deviceInfo(new DeviceInfo(this)), m_bleManager(new BleManager(this))
|
||||
, m_systemSleepMonitor(new SystemSleepMonitor(this))
|
||||
{
|
||||
QLoggingCategory::setFilterRules(QString("airpodsApp.debug=%1").arg(debugMode ? "true" : "false"));
|
||||
QLoggingCategory::setFilterRules(QString("Librepods.debug=%1").arg(debugMode ? "true" : "false"));
|
||||
LOG_INFO("Initializing AirPodsTrayApp");
|
||||
|
||||
// Initialize tray icon and connect signals
|
||||
@@ -50,16 +69,17 @@ public:
|
||||
|
||||
// Initialize MediaController and connect signals
|
||||
mediaController = new MediaController(this);
|
||||
connect(m_deviceInfo, &DeviceInfo::earDetectionStatusChanged, mediaController, &MediaController::handleEarDetection);
|
||||
connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange);
|
||||
mediaController->initializeMprisInterface();
|
||||
mediaController->followMediaChanges();
|
||||
|
||||
monitor = new BluetoothMonitor(this);
|
||||
connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected);
|
||||
connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected);
|
||||
|
||||
connect(m_bleManager, &BleManager::deviceFound, this, &AirPodsTrayApp::bleDeviceFound);
|
||||
connect(m_deviceInfo->getBattery(), &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged);
|
||||
connect(m_systemSleepMonitor, &SystemSleepMonitor::systemGoingToSleep, this, &AirPodsTrayApp::onSystemGoingToSleep);
|
||||
connect(m_systemSleepMonitor, &SystemSleepMonitor::systemWakingUp, this, &AirPodsTrayApp::onSystemWakingUp);
|
||||
|
||||
// Load settings
|
||||
CrossDevice.isEnabled = loadCrossDeviceEnabled();
|
||||
@@ -101,6 +121,7 @@ public:
|
||||
int retryAttempts() const { return m_retryAttempts; }
|
||||
bool hideOnStart() const { return m_hideOnStart; }
|
||||
DeviceInfo *deviceInfo() const { return m_deviceInfo; }
|
||||
QString phoneMacStatus() const { return m_phoneMacStatus; }
|
||||
|
||||
private:
|
||||
bool debugMode;
|
||||
@@ -151,6 +172,11 @@ public slots:
|
||||
|
||||
void setNoiseControlMode(NoiseControlMode mode)
|
||||
{
|
||||
if (m_deviceInfo->noiseControlMode() == mode)
|
||||
{
|
||||
LOG_INFO("Noise control mode is already set to: " << static_cast<int>(mode));
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Setting noise control mode to: " << mode);
|
||||
QByteArray packet = AirPodsPackets::NoiseControl::getPacketForMode(mode);
|
||||
writePacketToSocket(packet, "Noise control mode packet written: ");
|
||||
@@ -287,6 +313,51 @@ public slots:
|
||||
emit crossDeviceEnabledChanged(enabled);
|
||||
}
|
||||
|
||||
void setPhoneMac(const QString &mac)
|
||||
{
|
||||
if (mac.isEmpty()) {
|
||||
LOG_WARN("Empty MAC provided, ignoring");
|
||||
m_phoneMacStatus = QStringLiteral("No MAC provided (ignoring)");
|
||||
emit phoneMacStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic MAC address validation (accepts formats like AA:BB:CC:DD:EE:FF, AABBCCDDEEFF, AA-BB-CC-DD-EE-FF)
|
||||
QRegularExpression re("^([0-9A-Fa-f]{2}([-:]?)){5}[0-9A-Fa-f]{2}$");
|
||||
if (!re.match(mac).hasMatch()) {
|
||||
LOG_ERROR("Invalid MAC address format: " << mac);
|
||||
m_phoneMacStatus = QStringLiteral("Invalid MAC: ") + mac;
|
||||
emit phoneMacStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set environment variable for the running process
|
||||
qputenv("PHONE_MAC_ADDRESS", mac.toUtf8());
|
||||
LOG_INFO("PHONE_MAC_ADDRESS environment variable set to: " << mac);
|
||||
|
||||
m_phoneMacStatus = QStringLiteral("Updated MAC: ") + mac;
|
||||
emit phoneMacStatusChanged();
|
||||
|
||||
// Update QML context property so UI placeholders reflect the new value
|
||||
if (parent) {
|
||||
parent->rootContext()->setContextProperty("PHONE_MAC_ADDRESS", mac);
|
||||
}
|
||||
|
||||
// If a phone socket exists, restart connection using the new MAC
|
||||
if (phoneSocket && phoneSocket->isOpen()) {
|
||||
phoneSocket->close();
|
||||
phoneSocket->deleteLater();
|
||||
phoneSocket = nullptr;
|
||||
}
|
||||
connectToPhone();
|
||||
}
|
||||
|
||||
void updatePhoneMacStatus(const QString &status)
|
||||
{
|
||||
m_phoneMacStatus = status;
|
||||
emit phoneMacStatusChanged();
|
||||
}
|
||||
|
||||
bool writePacketToSocket(const QByteArray &packet, const QString &logMessage)
|
||||
{
|
||||
if (socket && socket->isOpen())
|
||||
@@ -314,6 +385,20 @@ public slots:
|
||||
int loadRetryAttempts() const { return m_settings->value("bluetooth/retryAttempts", 3).toInt(); }
|
||||
void saveRetryAttempts(int attempts) { m_settings->setValue("bluetooth/retryAttempts", attempts); }
|
||||
|
||||
void onSystemGoingToSleep()
|
||||
{
|
||||
if (m_bleManager->isScanning())
|
||||
{
|
||||
LOG_INFO("Stopping BLE scan before going to sleep");
|
||||
m_bleManager->stopScan();
|
||||
}
|
||||
}
|
||||
void onSystemWakingUp()
|
||||
{
|
||||
LOG_INFO("System is waking up, starting ble scan");
|
||||
m_bleManager->startScan();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void onTrayIconActivated()
|
||||
{
|
||||
@@ -379,6 +464,8 @@ private slots:
|
||||
|
||||
// Clear the device name and model
|
||||
m_deviceInfo->reset();
|
||||
m_bleManager->startScan();
|
||||
emit airPodsStatusChanged();
|
||||
|
||||
// Show system notification
|
||||
trayManager->showNotification(
|
||||
@@ -545,6 +632,7 @@ private slots:
|
||||
// Store the keys
|
||||
m_deviceInfo->setMagicAccIRK(keys.magicAccIRK);
|
||||
m_deviceInfo->setMagicAccEncKey(keys.magicAccEncKey);
|
||||
m_deviceInfo->saveToSettings(*m_settings);
|
||||
}
|
||||
// Get CA state
|
||||
else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) {
|
||||
@@ -559,7 +647,6 @@ private slots:
|
||||
{
|
||||
if (auto value = AirPodsPackets::NoiseControl::parseMode(data))
|
||||
{
|
||||
LOG_INFO("Received noise control mode: " << value.value());
|
||||
m_deviceInfo->setNoiseControlMode(value.value());
|
||||
LOG_INFO("Noise control mode received: " << m_deviceInfo->noiseControlMode());
|
||||
}
|
||||
@@ -567,26 +654,14 @@ private slots:
|
||||
// Ear Detection
|
||||
else if (data.size() == 8 && data.startsWith(AirPodsPackets::Parse::EAR_DETECTION))
|
||||
{
|
||||
char primary = data[6];
|
||||
char secondary = data[7];
|
||||
m_deviceInfo->setPrimaryInEar(data[6] == 0x00);
|
||||
m_deviceInfo->setSecondaryInEar(data[7] == 0x00);
|
||||
m_deviceInfo->setEarDetectionStatus(QString("Primary: %1, Secondary: %2")
|
||||
.arg(getEarStatus(primary), getEarStatus(secondary)));
|
||||
LOG_INFO("Ear detection status: " << m_deviceInfo->earDetectionStatus());
|
||||
m_deviceInfo->getEarDetection()->parseData(data);
|
||||
mediaController->handleEarDetection(m_deviceInfo->getEarDetection());
|
||||
}
|
||||
// Battery Status
|
||||
else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS))
|
||||
{
|
||||
m_deviceInfo->getBattery()->parsePacket(data);
|
||||
|
||||
int leftLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Left).level;
|
||||
int rightLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Right).level;
|
||||
int caseLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Case).level;
|
||||
m_deviceInfo->setBatteryStatus(QString("Left: %1%, Right: %2%, Case: %3%")
|
||||
.arg(leftLevel)
|
||||
.arg(rightLevel)
|
||||
.arg(caseLevel));
|
||||
m_deviceInfo->updateBatteryStatus();
|
||||
LOG_INFO("Battery status: " << m_deviceInfo->batteryStatus());
|
||||
}
|
||||
// Conversational Awareness Data
|
||||
@@ -600,10 +675,11 @@ private slots:
|
||||
parseMetadata(data);
|
||||
initiateMagicPairing();
|
||||
mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_"));
|
||||
if (m_deviceInfo->oneOrMorePodsInEar()) // AirPods get added as output device only after this
|
||||
if (m_deviceInfo->getEarDetection()->oneOrMorePodsInEar()) // AirPods get added as output device only after this
|
||||
{
|
||||
mediaController->activateA2dpProfile();
|
||||
}
|
||||
m_bleManager->stopScan();
|
||||
emit airPodsStatusChanged();
|
||||
}
|
||||
else if (data.startsWith(AirPodsPackets::OneBudANCMode::HEADER)) {
|
||||
@@ -628,11 +704,12 @@ private slots:
|
||||
LOG_INFO("Already connected to the phone");
|
||||
return;
|
||||
}
|
||||
QBluetoothAddress phoneAddress(PHONE_MAC_ADDRESS);
|
||||
QBluetoothAddress phoneAddress("00:00:00:00:00:00"); // Default address, will be overwritten if PHONE_MAC_ADDRESS is set
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
|
||||
if (!env.value("PHONE_MAC_ADDRESS").isEmpty())
|
||||
{
|
||||
QBluetoothAddress phoneAddress = QBluetoothAddress(env.value("PHONE_MAC_ADDRESS"));
|
||||
phoneAddress = QBluetoothAddress(env.value("PHONE_MAC_ADDRESS"));
|
||||
}
|
||||
phoneSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
||||
connect(phoneSocket, &QBluetoothSocket::connected, this, [this]() {
|
||||
@@ -733,6 +810,16 @@ private slots:
|
||||
QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data));
|
||||
}
|
||||
|
||||
void bleDeviceFound(const BleInfo &device)
|
||||
{
|
||||
if (BLEUtils::isValidIrkRpa(m_deviceInfo->magicAccIRK(), device.address)) {
|
||||
m_deviceInfo->setModel(device.modelName);
|
||||
auto decryptet = BLEUtils::decryptLastBytes(device.encryptedPayload, m_deviceInfo->magicAccEncKey());
|
||||
m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft, device.isThisPodInTheCase);
|
||||
m_deviceInfo->getEarDetection()->overrideEarDetectionStatus(device.isPrimaryInEar, device.isSecondaryInEar);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void handleMediaStateChange(MediaController::MediaState state) {
|
||||
if (state == MediaController::MediaState::Playing) {
|
||||
@@ -774,13 +861,6 @@ public:
|
||||
process.waitForFinished();
|
||||
QString output = process.readAllStandardOutput().trimmed();
|
||||
LOG_INFO("Bluetoothctl output: " << output);
|
||||
if (output.contains("Connection successful")) {
|
||||
LOG_INFO("Connection successful, proceeding with L2CAP connection");
|
||||
QBluetoothAddress btAddress(m_deviceInfo->bluetoothAddress());
|
||||
forceL2capConnection(btAddress);
|
||||
} else {
|
||||
LOG_ERROR("Connection failed, cannot proceed with L2CAP connection");
|
||||
}
|
||||
}
|
||||
QBluetoothLocalDevice localDevice;
|
||||
const QList<QBluetoothAddress> connectedDevices = localDevice.connectedDevices();
|
||||
@@ -795,33 +875,13 @@ public:
|
||||
LOG_WARN("AirPods not found among connected devices");
|
||||
}
|
||||
|
||||
void forceL2capConnection(const QBluetoothAddress &address) {
|
||||
LOG_INFO("Retrying L2CAP connection for up to 10 seconds...");
|
||||
QBluetoothDeviceInfo device(address, "", 0);
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
while (timer.elapsed() < 10000) {
|
||||
QProcess bcProcess;
|
||||
bcProcess.start("bluetoothctl", QStringList() << "connect" << address.toString());
|
||||
bcProcess.waitForFinished();
|
||||
QString output = bcProcess.readAllStandardOutput().trimmed();
|
||||
LOG_INFO("Bluetoothctl output: " << output);
|
||||
if (output.contains("Connection successful")) {
|
||||
connectToDevice(device);
|
||||
QThread::sleep(1);
|
||||
if (socket && socket->isOpen()) {
|
||||
LOG_INFO("Successfully connected to device: " << address.toString());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Connection attempt failed, retrying...");
|
||||
}
|
||||
}
|
||||
LOG_ERROR("Failed to connect to device within 10 seconds: " << address.toString());
|
||||
}
|
||||
|
||||
void initializeBluetooth() {
|
||||
connectToPhone();
|
||||
|
||||
m_deviceInfo->loadFromSettings(*m_settings);
|
||||
if (!areAirpodsConnected()) {
|
||||
m_bleManager->startScan();
|
||||
}
|
||||
}
|
||||
|
||||
void loadMainModule() {
|
||||
@@ -843,6 +903,7 @@ signals:
|
||||
void notificationsEnabledChanged(bool enabled);
|
||||
void retryAttemptsChanged(int attempts);
|
||||
void oneBudANCModeChanged(bool enabled);
|
||||
void phoneMacStatusChanged();
|
||||
|
||||
private:
|
||||
QBluetoothSocket *socket = nullptr;
|
||||
@@ -857,6 +918,9 @@ private:
|
||||
int m_retryAttempts = 3;
|
||||
bool m_hideOnStart = false;
|
||||
DeviceInfo *m_deviceInfo;
|
||||
BleManager *m_bleManager;
|
||||
SystemSleepMonitor *m_systemSleepMonitor = nullptr;
|
||||
QString m_phoneMacStatus;
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@@ -904,6 +968,17 @@ int main(int argc, char *argv[]) {
|
||||
qmlRegisterType<DeviceInfo>("me.kavishdevar.DeviceInfo", 1, 0, "DeviceInfo");
|
||||
AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine);
|
||||
engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp);
|
||||
|
||||
// Expose PHONE_MAC_ADDRESS environment variable to QML for placeholder in settings
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
QString phoneMacEnv = env.value("PHONE_MAC_ADDRESS", "");
|
||||
engine.rootContext()->setContextProperty("PHONE_MAC_ADDRESS", phoneMacEnv);
|
||||
// Initialize the visible status in the GUI
|
||||
trayApp->updatePhoneMacStatus(phoneMacEnv.isEmpty() ? QStringLiteral("No phone MAC set") : phoneMacEnv);
|
||||
}
|
||||
|
||||
engine.addImageProvider("qrcode", new QRCodeImageProvider());
|
||||
trayApp->loadMainModule();
|
||||
|
||||
QLocalServer server;
|
||||
|
||||
36
linux/main.h
@@ -1,36 +0,0 @@
|
||||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QBluetoothSocket>
|
||||
#include <QQuickWindow>
|
||||
#include <QDebug>
|
||||
#include <QInputDialog>
|
||||
#include <QQmlContext>
|
||||
#include <QLoggingCategory>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#define PHONE_MAC_ADDRESS "22:22:F5:BB:1C:A0"
|
||||
|
||||
#define MANUFACTURER_ID 0x1234
|
||||
#define MANUFACTURER_DATA "ALN_AirPods"
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "mediacontroller.h"
|
||||
#include "logger.h"
|
||||
#include "eardetection.hpp"
|
||||
#include "playerstatuswatcher.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QProcess>
|
||||
@@ -8,37 +10,9 @@
|
||||
#include <QDBusConnectionInterface>
|
||||
|
||||
MediaController::MediaController(QObject *parent) : QObject(parent) {
|
||||
// No additional initialization required here
|
||||
}
|
||||
|
||||
void MediaController::initializeMprisInterface() {
|
||||
QStringList services =
|
||||
QDBusConnection::sessionBus().interface()->registeredServiceNames();
|
||||
QString mprisService;
|
||||
|
||||
for (const QString &service : services) {
|
||||
if (service.startsWith("org.mpris.MediaPlayer2.") &&
|
||||
service != "org.mpris.MediaPlayer2") {
|
||||
mprisService = service;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mprisService.isEmpty()) {
|
||||
mprisInterface = new QDBusInterface(mprisService, "/org/mpris/MediaPlayer2",
|
||||
"org.mpris.MediaPlayer2.Player",
|
||||
QDBusConnection::sessionBus(), this);
|
||||
if (!mprisInterface->isValid()) {
|
||||
LOG_ERROR("Failed to initialize MPRIS interface for service: ") << mprisService;
|
||||
} else {
|
||||
LOG_INFO("Connected to MPRIS service: " << mprisService);
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("No active MPRIS media players found");
|
||||
}
|
||||
}
|
||||
|
||||
void MediaController::handleEarDetection(const QString &status)
|
||||
void MediaController::handleEarDetection(EarDetection *earDetection)
|
||||
{
|
||||
if (earDetectionBehavior == Disabled)
|
||||
{
|
||||
@@ -46,15 +20,8 @@ void MediaController::handleEarDetection(const QString &status)
|
||||
return;
|
||||
}
|
||||
|
||||
bool primaryInEar = false;
|
||||
bool secondaryInEar = false;
|
||||
|
||||
QStringList parts = status.split(", ");
|
||||
if (parts.size() == 2)
|
||||
{
|
||||
primaryInEar = parts[0].contains("In Ear");
|
||||
secondaryInEar = parts[1].contains("In Ear");
|
||||
}
|
||||
bool primaryInEar = earDetection->isPrimaryInEar();
|
||||
bool secondaryInEar = earDetection->isSecondaryInEar();
|
||||
|
||||
LOG_DEBUG("Ear detection status: primaryInEar="
|
||||
<< primaryInEar << ", secondaryInEar=" << secondaryInEar
|
||||
@@ -77,12 +44,7 @@ void MediaController::handleEarDetection(const QString &status)
|
||||
|
||||
if (shouldPause && isActiveOutputDeviceAirPods())
|
||||
{
|
||||
QProcess process;
|
||||
process.start("playerctl", QStringList() << "status");
|
||||
process.waitForFinished();
|
||||
QString playbackStatus = process.readAllStandardOutput().trimmed();
|
||||
LOG_DEBUG("Playback status: " << playbackStatus);
|
||||
if (playbackStatus == "Playing")
|
||||
if (getCurrentMediaState() == Playing)
|
||||
{
|
||||
pause();
|
||||
}
|
||||
@@ -97,17 +59,7 @@ void MediaController::handleEarDetection(const QString &status)
|
||||
// Resume if conditions are met and we previously paused
|
||||
if (shouldResume && wasPausedByApp && isActiveOutputDeviceAirPods())
|
||||
{
|
||||
int result = QProcess::execute("playerctl", QStringList() << "play");
|
||||
LOG_DEBUG("Executed 'playerctl play' with result: " << result);
|
||||
if (result == 0)
|
||||
{
|
||||
LOG_INFO("Resumed playback via Playerctl");
|
||||
wasPausedByApp = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("Failed to resume playback via Playerctl");
|
||||
}
|
||||
play();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -124,16 +76,14 @@ void MediaController::setEarDetectionBehavior(EarDetectionBehavior behavior)
|
||||
}
|
||||
|
||||
void MediaController::followMediaChanges() {
|
||||
playerctlProcess = new QProcess(this);
|
||||
connect(playerctlProcess, &QProcess::readyReadStandardOutput, this,
|
||||
[this]() {
|
||||
QString output =
|
||||
playerctlProcess->readAllStandardOutput().trimmed();
|
||||
LOG_DEBUG("Playerctl output: " << output);
|
||||
MediaState state = mediaStateFromPlayerctlOutput(output);
|
||||
playerStatusWatcher = new PlayerStatusWatcher("", this);
|
||||
connect(playerStatusWatcher, &PlayerStatusWatcher::playbackStatusChanged,
|
||||
this, [this](const QString &status)
|
||||
{
|
||||
LOG_DEBUG("Playback status changed: " << status);
|
||||
MediaState state = mediaStateFromPlayerctlOutput(status);
|
||||
emit mediaStateChanged(state);
|
||||
});
|
||||
playerctlProcess->start("playerctl", QStringList() << "--follow" << "status");
|
||||
}
|
||||
|
||||
bool MediaController::isActiveOutputDeviceAirPods() {
|
||||
@@ -222,7 +172,7 @@ void MediaController::setConnectedDeviceMacAddress(const QString &macAddress) {
|
||||
}
|
||||
|
||||
MediaController::MediaState MediaController::mediaStateFromPlayerctlOutput(
|
||||
const QString &output) {
|
||||
const QString &output) const {
|
||||
if (output == "Playing") {
|
||||
return MediaState::Playing;
|
||||
} else if (output == "Paused") {
|
||||
@@ -232,28 +182,106 @@ MediaController::MediaState MediaController::mediaStateFromPlayerctlOutput(
|
||||
}
|
||||
}
|
||||
|
||||
void MediaController::pause() {
|
||||
int result = QProcess::execute("playerctl", QStringList() << "pause");
|
||||
LOG_DEBUG("Executed 'playerctl pause' with result: " << result);
|
||||
if (result == 0)
|
||||
MediaController::MediaState MediaController::getCurrentMediaState() const
|
||||
{
|
||||
return mediaStateFromPlayerctlOutput(PlayerStatusWatcher::getCurrentPlaybackStatus(""));
|
||||
}
|
||||
|
||||
bool MediaController::sendMediaPlayerCommand(const QString &method)
|
||||
{
|
||||
// Connect to the session bus
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
|
||||
// Find available MPRIS-compatible media players
|
||||
QStringList services = bus.interface()->registeredServiceNames().value();
|
||||
QStringList mprisServices;
|
||||
for (const QString &service : services)
|
||||
{
|
||||
LOG_INFO("Paused playback via Playerctl");
|
||||
if (service.startsWith("org.mpris.MediaPlayer2."))
|
||||
{
|
||||
mprisServices << service;
|
||||
}
|
||||
}
|
||||
|
||||
if (mprisServices.isEmpty())
|
||||
{
|
||||
LOG_ERROR("No MPRIS-compatible media players found on DBus");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
// Try each MPRIS service until one succeeds
|
||||
for (const QString &service : mprisServices)
|
||||
{
|
||||
QDBusInterface playerInterface(
|
||||
service,
|
||||
"/org/mpris/MediaPlayer2",
|
||||
"org.mpris.MediaPlayer2.Player",
|
||||
bus);
|
||||
|
||||
if (!playerInterface.isValid())
|
||||
{
|
||||
LOG_ERROR("Invalid DBus interface for service: " << service);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send the Play or Pause command
|
||||
if (method == "Play" || method == "Pause")
|
||||
{
|
||||
QDBusReply<void> reply = playerInterface.call(method);
|
||||
if (reply.isValid())
|
||||
{
|
||||
LOG_INFO("Successfully sent " << method << " to " << service);
|
||||
success = true;
|
||||
break; // Exit after the first successful command
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("Failed to send " << method << " to " << service
|
||||
<< ": " << reply.error().message());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("Unsupported method: " << method);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
LOG_ERROR("No media player responded successfully to " << method);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void MediaController::play()
|
||||
{
|
||||
if (sendMediaPlayerCommand("Play"))
|
||||
{
|
||||
LOG_INFO("Resumed playback via DBus");
|
||||
wasPausedByApp = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("Failed to resume playback via DBus");
|
||||
}
|
||||
}
|
||||
|
||||
void MediaController::pause()
|
||||
{
|
||||
if (sendMediaPlayerCommand("Pause"))
|
||||
{
|
||||
LOG_INFO("Paused playback via DBus");
|
||||
wasPausedByApp = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR("Failed to pause playback via Playerctl");
|
||||
LOG_ERROR("Failed to pause playback via DBus");
|
||||
}
|
||||
}
|
||||
|
||||
MediaController::~MediaController() {
|
||||
if (playerctlProcess) {
|
||||
playerctlProcess->terminate();
|
||||
if (!playerctlProcess->waitForFinished()) {
|
||||
playerctlProcess->kill();
|
||||
playerctlProcess->waitForFinished(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString MediaController::getAudioDeviceName()
|
||||
@@ -1,10 +1,12 @@
|
||||
#ifndef MEDIACONTROLLER_H
|
||||
#define MEDIACONTROLLER_H
|
||||
|
||||
#include <QDBusInterface>
|
||||
#include <QObject>
|
||||
|
||||
class QProcess;
|
||||
class EarDetection;
|
||||
class PlayerStatusWatcher;
|
||||
class QDBusInterface;
|
||||
|
||||
class MediaController : public QObject
|
||||
{
|
||||
@@ -28,8 +30,7 @@ public:
|
||||
explicit MediaController(QObject *parent = nullptr);
|
||||
~MediaController();
|
||||
|
||||
void initializeMprisInterface();
|
||||
void handleEarDetection(const QString &status);
|
||||
void handleEarDetection(EarDetection*);
|
||||
void followMediaChanges();
|
||||
bool isActiveOutputDeviceAirPods();
|
||||
void handleConversationalAwareness(const QByteArray &data);
|
||||
@@ -40,22 +41,24 @@ public:
|
||||
void setEarDetectionBehavior(EarDetectionBehavior behavior);
|
||||
inline EarDetectionBehavior getEarDetectionBehavior() const { return earDetectionBehavior; }
|
||||
|
||||
void play();
|
||||
void pause();
|
||||
MediaState getCurrentMediaState() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void mediaStateChanged(MediaState state);
|
||||
|
||||
private:
|
||||
MediaState mediaStateFromPlayerctlOutput(const QString &output);
|
||||
MediaState mediaStateFromPlayerctlOutput(const QString &output) const;
|
||||
QString getAudioDeviceName();
|
||||
bool sendMediaPlayerCommand(const QString &method);
|
||||
|
||||
QDBusInterface *mprisInterface = nullptr;
|
||||
QProcess *playerctlProcess = nullptr;
|
||||
bool wasPausedByApp = false;
|
||||
int initialVolume = -1;
|
||||
QString connectedDeviceMacAddress;
|
||||
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;
|
||||
QString m_deviceOutputName;
|
||||
PlayerStatusWatcher *playerStatusWatcher = nullptr;
|
||||
};
|
||||
|
||||
#endif // MEDIACONTROLLER_H
|
||||
70
linux/media/playerstatuswatcher.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "playerstatuswatcher.h"
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QVariantMap>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusConnectionInterface>
|
||||
|
||||
PlayerStatusWatcher::PlayerStatusWatcher(const QString &playerService, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_playerService(playerService),
|
||||
m_iface(new QDBusInterface(playerService, "/org/mpris/MediaPlayer2",
|
||||
"org.mpris.MediaPlayer2.Player", QDBusConnection::sessionBus(), this)),
|
||||
m_serviceWatcher(new QDBusServiceWatcher(playerService, QDBusConnection::sessionBus(),
|
||||
QDBusServiceWatcher::WatchForOwnerChange, this))
|
||||
{
|
||||
QDBusConnection::sessionBus().connect(
|
||||
playerService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))
|
||||
);
|
||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
|
||||
this, &PlayerStatusWatcher::onServiceOwnerChanged);
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
void PlayerStatusWatcher::onPropertiesChanged(const QString &interface,
|
||||
const QVariantMap &changed,
|
||||
const QStringList &)
|
||||
{
|
||||
if (interface == "org.mpris.MediaPlayer2.Player" && changed.contains("PlaybackStatus")) {
|
||||
emit playbackStatusChanged(changed.value("PlaybackStatus").toString());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerStatusWatcher::updateStatus() {
|
||||
QVariant reply = m_iface->property("PlaybackStatus");
|
||||
if (reply.isValid()) {
|
||||
emit playbackStatusChanged(reply.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerStatusWatcher::onServiceOwnerChanged(const QString &name, const QString &, const QString &newOwner)
|
||||
{
|
||||
if (name == m_playerService && newOwner.isEmpty()) {
|
||||
emit playbackStatusChanged(""); // player disappeared
|
||||
} else if (name == m_playerService && !newOwner.isEmpty()) {
|
||||
updateStatus(); // player appeared/reappeared
|
||||
}
|
||||
}
|
||||
|
||||
QString PlayerStatusWatcher::getCurrentPlaybackStatus(const QString &playerService)
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
QStringList services = bus.interface()->registeredServiceNames().value();
|
||||
|
||||
for (const QString &service : services) {
|
||||
if (service.startsWith("org.mpris.MediaPlayer2.")) {
|
||||
QDBusInterface iface(service, "/org/mpris/MediaPlayer2",
|
||||
"org.mpris.MediaPlayer2.Player", bus);
|
||||
|
||||
if (iface.isValid()) {
|
||||
QVariant status = iface.property("PlaybackStatus");
|
||||
if (status.isValid() && status.toString() == "Playing") {
|
||||
return status.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
25
linux/media/playerstatuswatcher.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusServiceWatcher>
|
||||
|
||||
class PlayerStatusWatcher : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PlayerStatusWatcher(const QString &playerService, QObject *parent = nullptr);
|
||||
static QString getCurrentPlaybackStatus(const QString &playerService);
|
||||
|
||||
signals:
|
||||
void playbackStatusChanged(const QString &status);
|
||||
|
||||
private slots:
|
||||
void onPropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &);
|
||||
void onServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner);
|
||||
|
||||
private:
|
||||
void updateStatus();
|
||||
QString m_playerService;
|
||||
QDBusInterface *m_iface;
|
||||
QDBusServiceWatcher *m_serviceWatcher;
|
||||
};
|
||||
77
linux/playerstatuswatcher.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "media/playerstatuswatcher.h"
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QVariantMap>
|
||||
#include <QDBusReply>
|
||||
|
||||
PlayerStatusWatcher::PlayerStatusWatcher(const QString &playerService, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_playerService(playerService),
|
||||
m_iface(new QDBusInterface(playerService, "/org/mpris/MediaPlayer2",
|
||||
"org.mpris.MediaPlayer2.Player", QDBusConnection::sessionBus(), this)),
|
||||
m_serviceWatcher(new QDBusServiceWatcher(playerService, QDBusConnection::sessionBus(),
|
||||
QDBusServiceWatcher::WatchForOwnerChange, this))
|
||||
{
|
||||
// Register this object on the session bus to receive D-Bus messages
|
||||
QDBusConnection::sessionBus().registerObject("/PlayerStatusWatcher", this,
|
||||
QDBusConnection::ExportAllSlots);
|
||||
|
||||
QDBusConnection::sessionBus().connect(
|
||||
playerService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))
|
||||
);
|
||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
|
||||
this, &PlayerStatusWatcher::onServiceOwnerChanged);
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
void PlayerStatusWatcher::onPropertiesChanged(const QString &interface,
|
||||
const QVariantMap &changed,
|
||||
const QStringList &)
|
||||
{
|
||||
// Get the service name of the sender
|
||||
QString sender = message().service();
|
||||
|
||||
// Skip if it's a KDE Connect player
|
||||
if (sender.contains("kdeconnect", Qt::CaseInsensitive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (interface == "org.mpris.MediaPlayer2.Player" && changed.contains("PlaybackStatus")) {
|
||||
emit playbackStatusChanged(changed.value("PlaybackStatus").toString());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerStatusWatcher::updateStatus() {
|
||||
QVariant reply = m_iface->property("PlaybackStatus");
|
||||
if (reply.isValid()) {
|
||||
emit playbackStatusChanged(reply.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerStatusWatcher::onServiceOwnerChanged(const QString &name, const QString &, const QString &newOwner)
|
||||
{
|
||||
if (name == m_playerService && newOwner.isEmpty()) {
|
||||
emit playbackStatusChanged(""); // player disappeared
|
||||
} else if (name == m_playerService && !newOwner.isEmpty()) {
|
||||
updateStatus(); // player appeared/reappeared
|
||||
}
|
||||
}
|
||||
|
||||
QString PlayerStatusWatcher::getCurrentPlaybackStatus(const QString &playerService)
|
||||
{
|
||||
QDBusInterface iface(
|
||||
playerService,
|
||||
"/org/mpris/MediaPlayer2",
|
||||
"org.mpris.MediaPlayer2.Player",
|
||||
QDBusConnection::sessionBus());
|
||||
QVariant reply = iface.property("PlaybackStatus");
|
||||
if (reply.isValid())
|
||||
{
|
||||
return reply.toString(); // "Playing", "Paused", "Stopped"
|
||||
}
|
||||
else
|
||||
{
|
||||
return QString(); // or handle error as needed
|
||||
}
|
||||
}
|
||||
49
linux/systemsleepmonitor.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef SYSTEMSLEEPMONITOR_HPP
|
||||
#define SYSTEMSLEEPMONITOR_HPP
|
||||
|
||||
#include <QObject>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMessage>
|
||||
#include <QDebug>
|
||||
|
||||
class SystemSleepMonitor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SystemSleepMonitor(QObject *parent = nullptr) : QObject(parent) {
|
||||
// Connect to the system D-Bus
|
||||
QDBusConnection systemBus = QDBusConnection::systemBus();
|
||||
if (!systemBus.isConnected()) {
|
||||
qWarning() << "Cannot connect to system D-Bus";
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to PrepareForSleep signal from logind
|
||||
systemBus.connect(
|
||||
"org.freedesktop.login1",
|
||||
"/org/freedesktop/login1",
|
||||
"org.freedesktop.login1.Manager",
|
||||
"PrepareForSleep",
|
||||
this,
|
||||
SLOT(handlePrepareForSleep(bool))
|
||||
);
|
||||
}
|
||||
|
||||
~SystemSleepMonitor() override = default;
|
||||
|
||||
signals:
|
||||
void systemGoingToSleep();
|
||||
void systemWakingUp();
|
||||
|
||||
private slots:
|
||||
void handlePrepareForSleep(bool sleeping) {
|
||||
if (sleeping) {
|
||||
emit systemGoingToSleep();
|
||||
} else {
|
||||
emit systemWakingUp();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SYSTEMSLEEPMONITOR_HPP
|
||||
829
linux/thirdparty/QR-Code-generator/qrcodegen.cpp
vendored
Normal file
@@ -0,0 +1,829 @@
|
||||
/*
|
||||
* QR Code generator library (C++)
|
||||
*
|
||||
* Copyright (c) Project Nayuki. (MIT License)
|
||||
* https://www.nayuki.io/page/qr-code-generator-library
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - The Software is provided "as is", without warranty of any kind, express or
|
||||
* implied, including but not limited to the warranties of merchantability,
|
||||
* fitness for a particular purpose and noninfringement. In no event shall the
|
||||
* authors or copyright holders be liable for any claim, damages or other
|
||||
* liability, whether in an action of contract, tort or otherwise, arising from,
|
||||
* out of or in connection with the Software or the use or other dealings in the
|
||||
* Software.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include "qrcodegen.hpp"
|
||||
|
||||
using std::int8_t;
|
||||
using std::uint8_t;
|
||||
using std::size_t;
|
||||
using std::vector;
|
||||
|
||||
|
||||
namespace qrcodegen {
|
||||
|
||||
/*---- Class QrSegment ----*/
|
||||
|
||||
QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) :
|
||||
modeBits(mode) {
|
||||
numBitsCharCount[0] = cc0;
|
||||
numBitsCharCount[1] = cc1;
|
||||
numBitsCharCount[2] = cc2;
|
||||
}
|
||||
|
||||
|
||||
int QrSegment::Mode::getModeBits() const {
|
||||
return modeBits;
|
||||
}
|
||||
|
||||
|
||||
int QrSegment::Mode::numCharCountBits(int ver) const {
|
||||
return numBitsCharCount[(ver + 7) / 17];
|
||||
}
|
||||
|
||||
|
||||
const QrSegment::Mode QrSegment::Mode::NUMERIC (0x1, 10, 12, 14);
|
||||
const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13);
|
||||
const QrSegment::Mode QrSegment::Mode::BYTE (0x4, 8, 16, 16);
|
||||
const QrSegment::Mode QrSegment::Mode::KANJI (0x8, 8, 10, 12);
|
||||
const QrSegment::Mode QrSegment::Mode::ECI (0x7, 0, 0, 0);
|
||||
|
||||
|
||||
QrSegment QrSegment::makeBytes(const vector<uint8_t> &data) {
|
||||
if (data.size() > static_cast<unsigned int>(INT_MAX))
|
||||
throw std::length_error("Data too long");
|
||||
BitBuffer bb;
|
||||
for (uint8_t b : data)
|
||||
bb.appendBits(b, 8);
|
||||
return QrSegment(Mode::BYTE, static_cast<int>(data.size()), std::move(bb));
|
||||
}
|
||||
|
||||
|
||||
QrSegment QrSegment::makeNumeric(const char *digits) {
|
||||
BitBuffer bb;
|
||||
int accumData = 0;
|
||||
int accumCount = 0;
|
||||
int charCount = 0;
|
||||
for (; *digits != '\0'; digits++, charCount++) {
|
||||
char c = *digits;
|
||||
if (c < '0' || c > '9')
|
||||
throw std::domain_error("String contains non-numeric characters");
|
||||
accumData = accumData * 10 + (c - '0');
|
||||
accumCount++;
|
||||
if (accumCount == 3) {
|
||||
bb.appendBits(static_cast<uint32_t>(accumData), 10);
|
||||
accumData = 0;
|
||||
accumCount = 0;
|
||||
}
|
||||
}
|
||||
if (accumCount > 0) // 1 or 2 digits remaining
|
||||
bb.appendBits(static_cast<uint32_t>(accumData), accumCount * 3 + 1);
|
||||
return QrSegment(Mode::NUMERIC, charCount, std::move(bb));
|
||||
}
|
||||
|
||||
|
||||
QrSegment QrSegment::makeAlphanumeric(const char *text) {
|
||||
BitBuffer bb;
|
||||
int accumData = 0;
|
||||
int accumCount = 0;
|
||||
int charCount = 0;
|
||||
for (; *text != '\0'; text++, charCount++) {
|
||||
const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text);
|
||||
if (temp == nullptr)
|
||||
throw std::domain_error("String contains unencodable characters in alphanumeric mode");
|
||||
accumData = accumData * 45 + static_cast<int>(temp - ALPHANUMERIC_CHARSET);
|
||||
accumCount++;
|
||||
if (accumCount == 2) {
|
||||
bb.appendBits(static_cast<uint32_t>(accumData), 11);
|
||||
accumData = 0;
|
||||
accumCount = 0;
|
||||
}
|
||||
}
|
||||
if (accumCount > 0) // 1 character remaining
|
||||
bb.appendBits(static_cast<uint32_t>(accumData), 6);
|
||||
return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb));
|
||||
}
|
||||
|
||||
|
||||
vector<QrSegment> QrSegment::makeSegments(const char *text) {
|
||||
// Select the most efficient segment encoding automatically
|
||||
vector<QrSegment> result;
|
||||
if (*text == '\0'); // Leave result empty
|
||||
else if (isNumeric(text))
|
||||
result.push_back(makeNumeric(text));
|
||||
else if (isAlphanumeric(text))
|
||||
result.push_back(makeAlphanumeric(text));
|
||||
else {
|
||||
vector<uint8_t> bytes;
|
||||
for (; *text != '\0'; text++)
|
||||
bytes.push_back(static_cast<uint8_t>(*text));
|
||||
result.push_back(makeBytes(bytes));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
QrSegment QrSegment::makeEci(long assignVal) {
|
||||
BitBuffer bb;
|
||||
if (assignVal < 0)
|
||||
throw std::domain_error("ECI assignment value out of range");
|
||||
else if (assignVal < (1 << 7))
|
||||
bb.appendBits(static_cast<uint32_t>(assignVal), 8);
|
||||
else if (assignVal < (1 << 14)) {
|
||||
bb.appendBits(2, 2);
|
||||
bb.appendBits(static_cast<uint32_t>(assignVal), 14);
|
||||
} else if (assignVal < 1000000L) {
|
||||
bb.appendBits(6, 3);
|
||||
bb.appendBits(static_cast<uint32_t>(assignVal), 21);
|
||||
} else
|
||||
throw std::domain_error("ECI assignment value out of range");
|
||||
return QrSegment(Mode::ECI, 0, std::move(bb));
|
||||
}
|
||||
|
||||
|
||||
QrSegment::QrSegment(const Mode &md, int numCh, const std::vector<bool> &dt) :
|
||||
mode(&md),
|
||||
numChars(numCh),
|
||||
data(dt) {
|
||||
if (numCh < 0)
|
||||
throw std::domain_error("Invalid value");
|
||||
}
|
||||
|
||||
|
||||
QrSegment::QrSegment(const Mode &md, int numCh, std::vector<bool> &&dt) :
|
||||
mode(&md),
|
||||
numChars(numCh),
|
||||
data(std::move(dt)) {
|
||||
if (numCh < 0)
|
||||
throw std::domain_error("Invalid value");
|
||||
}
|
||||
|
||||
|
||||
int QrSegment::getTotalBits(const vector<QrSegment> &segs, int version) {
|
||||
int result = 0;
|
||||
for (const QrSegment &seg : segs) {
|
||||
int ccbits = seg.mode->numCharCountBits(version);
|
||||
if (seg.numChars >= (1L << ccbits))
|
||||
return -1; // The segment's length doesn't fit the field's bit width
|
||||
if (4 + ccbits > INT_MAX - result)
|
||||
return -1; // The sum will overflow an int type
|
||||
result += 4 + ccbits;
|
||||
if (seg.data.size() > static_cast<unsigned int>(INT_MAX - result))
|
||||
return -1; // The sum will overflow an int type
|
||||
result += static_cast<int>(seg.data.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool QrSegment::isNumeric(const char *text) {
|
||||
for (; *text != '\0'; text++) {
|
||||
char c = *text;
|
||||
if (c < '0' || c > '9')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool QrSegment::isAlphanumeric(const char *text) {
|
||||
for (; *text != '\0'; text++) {
|
||||
if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const QrSegment::Mode &QrSegment::getMode() const {
|
||||
return *mode;
|
||||
}
|
||||
|
||||
|
||||
int QrSegment::getNumChars() const {
|
||||
return numChars;
|
||||
}
|
||||
|
||||
|
||||
const std::vector<bool> &QrSegment::getData() const {
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
const char *QrSegment::ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
||||
|
||||
|
||||
|
||||
/*---- Class QrCode ----*/
|
||||
|
||||
int QrCode::getFormatBits(Ecc ecl) {
|
||||
switch (ecl) {
|
||||
case Ecc::LOW : return 1;
|
||||
case Ecc::MEDIUM : return 0;
|
||||
case Ecc::QUARTILE: return 3;
|
||||
case Ecc::HIGH : return 2;
|
||||
default: throw std::logic_error("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QrCode QrCode::encodeText(const char *text, Ecc ecl) {
|
||||
vector<QrSegment> segs = QrSegment::makeSegments(text);
|
||||
return encodeSegments(segs, ecl);
|
||||
}
|
||||
|
||||
|
||||
QrCode QrCode::encodeBinary(const vector<uint8_t> &data, Ecc ecl) {
|
||||
vector<QrSegment> segs{QrSegment::makeBytes(data)};
|
||||
return encodeSegments(segs, ecl);
|
||||
}
|
||||
|
||||
|
||||
QrCode QrCode::encodeSegments(const vector<QrSegment> &segs, Ecc ecl,
|
||||
int minVersion, int maxVersion, int mask, bool boostEcl) {
|
||||
if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7)
|
||||
throw std::invalid_argument("Invalid value");
|
||||
|
||||
// Find the minimal version number to use
|
||||
int version, dataUsedBits;
|
||||
for (version = minVersion; ; version++) {
|
||||
int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available
|
||||
dataUsedBits = QrSegment::getTotalBits(segs, version);
|
||||
if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits)
|
||||
break; // This version number is found to be suitable
|
||||
if (version >= maxVersion) { // All versions in the range could not fit the given data
|
||||
std::ostringstream sb;
|
||||
if (dataUsedBits == -1)
|
||||
sb << "Segment too long";
|
||||
else {
|
||||
sb << "Data length = " << dataUsedBits << " bits, ";
|
||||
sb << "Max capacity = " << dataCapacityBits << " bits";
|
||||
}
|
||||
throw data_too_long(sb.str());
|
||||
}
|
||||
}
|
||||
assert(dataUsedBits != -1);
|
||||
|
||||
// Increase the error correction level while the data still fits in the current version number
|
||||
for (Ecc newEcl : {Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) { // From low to high
|
||||
if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
|
||||
ecl = newEcl;
|
||||
}
|
||||
|
||||
// Concatenate all segments to create the data bit string
|
||||
BitBuffer bb;
|
||||
for (const QrSegment &seg : segs) {
|
||||
bb.appendBits(static_cast<uint32_t>(seg.getMode().getModeBits()), 4);
|
||||
bb.appendBits(static_cast<uint32_t>(seg.getNumChars()), seg.getMode().numCharCountBits(version));
|
||||
bb.insert(bb.end(), seg.getData().begin(), seg.getData().end());
|
||||
}
|
||||
assert(bb.size() == static_cast<unsigned int>(dataUsedBits));
|
||||
|
||||
// Add terminator and pad up to a byte if applicable
|
||||
size_t dataCapacityBits = static_cast<size_t>(getNumDataCodewords(version, ecl)) * 8;
|
||||
assert(bb.size() <= dataCapacityBits);
|
||||
bb.appendBits(0, std::min(4, static_cast<int>(dataCapacityBits - bb.size())));
|
||||
bb.appendBits(0, (8 - static_cast<int>(bb.size() % 8)) % 8);
|
||||
assert(bb.size() % 8 == 0);
|
||||
|
||||
// Pad with alternating bytes until data capacity is reached
|
||||
for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; padByte ^= 0xEC ^ 0x11)
|
||||
bb.appendBits(padByte, 8);
|
||||
|
||||
// Pack bits into bytes in big endian
|
||||
vector<uint8_t> dataCodewords(bb.size() / 8);
|
||||
for (size_t i = 0; i < bb.size(); i++)
|
||||
dataCodewords.at(i >> 3) |= (bb.at(i) ? 1 : 0) << (7 - (i & 7));
|
||||
|
||||
// Create the QR Code object
|
||||
return QrCode(version, ecl, dataCodewords, mask);
|
||||
}
|
||||
|
||||
|
||||
QrCode::QrCode(int ver, Ecc ecl, const vector<uint8_t> &dataCodewords, int msk) :
|
||||
// Initialize fields and check arguments
|
||||
version(ver),
|
||||
errorCorrectionLevel(ecl) {
|
||||
if (ver < MIN_VERSION || ver > MAX_VERSION)
|
||||
throw std::domain_error("Version value out of range");
|
||||
if (msk < -1 || msk > 7)
|
||||
throw std::domain_error("Mask value out of range");
|
||||
size = ver * 4 + 17;
|
||||
size_t sz = static_cast<size_t>(size);
|
||||
modules = vector<vector<bool> >(sz, vector<bool>(sz)); // Initially all light
|
||||
isFunction = vector<vector<bool> >(sz, vector<bool>(sz));
|
||||
|
||||
// Compute ECC, draw modules
|
||||
drawFunctionPatterns();
|
||||
const vector<uint8_t> allCodewords = addEccAndInterleave(dataCodewords);
|
||||
drawCodewords(allCodewords);
|
||||
|
||||
// Do masking
|
||||
if (msk == -1) { // Automatically choose best mask
|
||||
long minPenalty = LONG_MAX;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
applyMask(i);
|
||||
drawFormatBits(i);
|
||||
long penalty = getPenaltyScore();
|
||||
if (penalty < minPenalty) {
|
||||
msk = i;
|
||||
minPenalty = penalty;
|
||||
}
|
||||
applyMask(i); // Undoes the mask due to XOR
|
||||
}
|
||||
}
|
||||
assert(0 <= msk && msk <= 7);
|
||||
mask = msk;
|
||||
applyMask(msk); // Apply the final choice of mask
|
||||
drawFormatBits(msk); // Overwrite old format bits
|
||||
|
||||
isFunction.clear();
|
||||
isFunction.shrink_to_fit();
|
||||
}
|
||||
|
||||
|
||||
int QrCode::getVersion() const {
|
||||
return version;
|
||||
}
|
||||
|
||||
|
||||
int QrCode::getSize() const {
|
||||
return size;
|
||||
}
|
||||
|
||||
|
||||
QrCode::Ecc QrCode::getErrorCorrectionLevel() const {
|
||||
return errorCorrectionLevel;
|
||||
}
|
||||
|
||||
|
||||
int QrCode::getMask() const {
|
||||
return mask;
|
||||
}
|
||||
|
||||
|
||||
bool QrCode::getModule(int x, int y) const {
|
||||
return 0 <= x && x < size && 0 <= y && y < size && module(x, y);
|
||||
}
|
||||
|
||||
|
||||
void QrCode::drawFunctionPatterns() {
|
||||
// Draw horizontal and vertical timing patterns
|
||||
for (int i = 0; i < size; i++) {
|
||||
setFunctionModule(6, i, i % 2 == 0);
|
||||
setFunctionModule(i, 6, i % 2 == 0);
|
||||
}
|
||||
|
||||
// Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
|
||||
drawFinderPattern(3, 3);
|
||||
drawFinderPattern(size - 4, 3);
|
||||
drawFinderPattern(3, size - 4);
|
||||
|
||||
// Draw numerous alignment patterns
|
||||
const vector<int> alignPatPos = getAlignmentPatternPositions();
|
||||
size_t numAlign = alignPatPos.size();
|
||||
for (size_t i = 0; i < numAlign; i++) {
|
||||
for (size_t j = 0; j < numAlign; j++) {
|
||||
// Don't draw on the three finder corners
|
||||
if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0)))
|
||||
drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw configuration data
|
||||
drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
|
||||
drawVersion();
|
||||
}
|
||||
|
||||
|
||||
void QrCode::drawFormatBits(int msk) {
|
||||
// Calculate error correction code and pack bits
|
||||
int data = getFormatBits(errorCorrectionLevel) << 3 | msk; // errCorrLvl is uint2, msk is uint3
|
||||
int rem = data;
|
||||
for (int i = 0; i < 10; i++)
|
||||
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
|
||||
int bits = (data << 10 | rem) ^ 0x5412; // uint15
|
||||
assert(bits >> 15 == 0);
|
||||
|
||||
// Draw first copy
|
||||
for (int i = 0; i <= 5; i++)
|
||||
setFunctionModule(8, i, getBit(bits, i));
|
||||
setFunctionModule(8, 7, getBit(bits, 6));
|
||||
setFunctionModule(8, 8, getBit(bits, 7));
|
||||
setFunctionModule(7, 8, getBit(bits, 8));
|
||||
for (int i = 9; i < 15; i++)
|
||||
setFunctionModule(14 - i, 8, getBit(bits, i));
|
||||
|
||||
// Draw second copy
|
||||
for (int i = 0; i < 8; i++)
|
||||
setFunctionModule(size - 1 - i, 8, getBit(bits, i));
|
||||
for (int i = 8; i < 15; i++)
|
||||
setFunctionModule(8, size - 15 + i, getBit(bits, i));
|
||||
setFunctionModule(8, size - 8, true); // Always dark
|
||||
}
|
||||
|
||||
|
||||
void QrCode::drawVersion() {
|
||||
if (version < 7)
|
||||
return;
|
||||
|
||||
// Calculate error correction code and pack bits
|
||||
int rem = version; // version is uint6, in the range [7, 40]
|
||||
for (int i = 0; i < 12; i++)
|
||||
rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
|
||||
long bits = static_cast<long>(version) << 12 | rem; // uint18
|
||||
assert(bits >> 18 == 0);
|
||||
|
||||
// Draw two copies
|
||||
for (int i = 0; i < 18; i++) {
|
||||
bool bit = getBit(bits, i);
|
||||
int a = size - 11 + i % 3;
|
||||
int b = i / 3;
|
||||
setFunctionModule(a, b, bit);
|
||||
setFunctionModule(b, a, bit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void QrCode::drawFinderPattern(int x, int y) {
|
||||
for (int dy = -4; dy <= 4; dy++) {
|
||||
for (int dx = -4; dx <= 4; dx++) {
|
||||
int dist = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm
|
||||
int xx = x + dx, yy = y + dy;
|
||||
if (0 <= xx && xx < size && 0 <= yy && yy < size)
|
||||
setFunctionModule(xx, yy, dist != 2 && dist != 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void QrCode::drawAlignmentPattern(int x, int y) {
|
||||
for (int dy = -2; dy <= 2; dy++) {
|
||||
for (int dx = -2; dx <= 2; dx++)
|
||||
setFunctionModule(x + dx, y + dy, std::max(std::abs(dx), std::abs(dy)) != 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void QrCode::setFunctionModule(int x, int y, bool isDark) {
|
||||
size_t ux = static_cast<size_t>(x);
|
||||
size_t uy = static_cast<size_t>(y);
|
||||
modules .at(uy).at(ux) = isDark;
|
||||
isFunction.at(uy).at(ux) = true;
|
||||
}
|
||||
|
||||
|
||||
bool QrCode::module(int x, int y) const {
|
||||
return modules.at(static_cast<size_t>(y)).at(static_cast<size_t>(x));
|
||||
}
|
||||
|
||||
|
||||
vector<uint8_t> QrCode::addEccAndInterleave(const vector<uint8_t> &data) const {
|
||||
if (data.size() != static_cast<unsigned int>(getNumDataCodewords(version, errorCorrectionLevel)))
|
||||
throw std::invalid_argument("Invalid argument");
|
||||
|
||||
// Calculate parameter numbers
|
||||
int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[static_cast<int>(errorCorrectionLevel)][version];
|
||||
int blockEccLen = ECC_CODEWORDS_PER_BLOCK [static_cast<int>(errorCorrectionLevel)][version];
|
||||
int rawCodewords = getNumRawDataModules(version) / 8;
|
||||
int numShortBlocks = numBlocks - rawCodewords % numBlocks;
|
||||
int shortBlockLen = rawCodewords / numBlocks;
|
||||
|
||||
// Split data into blocks and append ECC to each block
|
||||
vector<vector<uint8_t> > blocks;
|
||||
const vector<uint8_t> rsDiv = reedSolomonComputeDivisor(blockEccLen);
|
||||
for (int i = 0, k = 0; i < numBlocks; i++) {
|
||||
vector<uint8_t> dat(data.cbegin() + k, data.cbegin() + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1)));
|
||||
k += static_cast<int>(dat.size());
|
||||
const vector<uint8_t> ecc = reedSolomonComputeRemainder(dat, rsDiv);
|
||||
if (i < numShortBlocks)
|
||||
dat.push_back(0);
|
||||
dat.insert(dat.end(), ecc.cbegin(), ecc.cend());
|
||||
blocks.push_back(std::move(dat));
|
||||
}
|
||||
|
||||
// Interleave (not concatenate) the bytes from every block into a single sequence
|
||||
vector<uint8_t> result;
|
||||
for (size_t i = 0; i < blocks.at(0).size(); i++) {
|
||||
for (size_t j = 0; j < blocks.size(); j++) {
|
||||
// Skip the padding byte in short blocks
|
||||
if (i != static_cast<unsigned int>(shortBlockLen - blockEccLen) || j >= static_cast<unsigned int>(numShortBlocks))
|
||||
result.push_back(blocks.at(j).at(i));
|
||||
}
|
||||
}
|
||||
assert(result.size() == static_cast<unsigned int>(rawCodewords));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
void QrCode::drawCodewords(const vector<uint8_t> &data) {
|
||||
if (data.size() != static_cast<unsigned int>(getNumRawDataModules(version) / 8))
|
||||
throw std::invalid_argument("Invalid argument");
|
||||
|
||||
size_t i = 0; // Bit index into the data
|
||||
// Do the funny zigzag scan
|
||||
for (int right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair
|
||||
if (right == 6)
|
||||
right = 5;
|
||||
for (int vert = 0; vert < size; vert++) { // Vertical counter
|
||||
for (int j = 0; j < 2; j++) {
|
||||
size_t x = static_cast<size_t>(right - j); // Actual x coordinate
|
||||
bool upward = ((right + 1) & 2) == 0;
|
||||
size_t y = static_cast<size_t>(upward ? size - 1 - vert : vert); // Actual y coordinate
|
||||
if (!isFunction.at(y).at(x) && i < data.size() * 8) {
|
||||
modules.at(y).at(x) = getBit(data.at(i >> 3), 7 - static_cast<int>(i & 7));
|
||||
i++;
|
||||
}
|
||||
// If this QR Code has any remainder bits (0 to 7), they were assigned as
|
||||
// 0/false/light by the constructor and are left unchanged by this method
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(i == data.size() * 8);
|
||||
}
|
||||
|
||||
|
||||
void QrCode::applyMask(int msk) {
|
||||
if (msk < 0 || msk > 7)
|
||||
throw std::domain_error("Mask value out of range");
|
||||
size_t sz = static_cast<size_t>(size);
|
||||
for (size_t y = 0; y < sz; y++) {
|
||||
for (size_t x = 0; x < sz; x++) {
|
||||
bool invert;
|
||||
switch (msk) {
|
||||
case 0: invert = (x + y) % 2 == 0; break;
|
||||
case 1: invert = y % 2 == 0; break;
|
||||
case 2: invert = x % 3 == 0; break;
|
||||
case 3: invert = (x + y) % 3 == 0; break;
|
||||
case 4: invert = (x / 3 + y / 2) % 2 == 0; break;
|
||||
case 5: invert = x * y % 2 + x * y % 3 == 0; break;
|
||||
case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break;
|
||||
case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break;
|
||||
default: throw std::logic_error("Unreachable");
|
||||
}
|
||||
modules.at(y).at(x) = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
long QrCode::getPenaltyScore() const {
|
||||
long result = 0;
|
||||
|
||||
// Adjacent modules in row having same color, and finder-like patterns
|
||||
for (int y = 0; y < size; y++) {
|
||||
bool runColor = false;
|
||||
int runX = 0;
|
||||
std::array<int,7> runHistory = {};
|
||||
for (int x = 0; x < size; x++) {
|
||||
if (module(x, y) == runColor) {
|
||||
runX++;
|
||||
if (runX == 5)
|
||||
result += PENALTY_N1;
|
||||
else if (runX > 5)
|
||||
result++;
|
||||
} else {
|
||||
finderPenaltyAddHistory(runX, runHistory);
|
||||
if (!runColor)
|
||||
result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
||||
runColor = module(x, y);
|
||||
runX = 1;
|
||||
}
|
||||
}
|
||||
result += finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
|
||||
}
|
||||
// Adjacent modules in column having same color, and finder-like patterns
|
||||
for (int x = 0; x < size; x++) {
|
||||
bool runColor = false;
|
||||
int runY = 0;
|
||||
std::array<int,7> runHistory = {};
|
||||
for (int y = 0; y < size; y++) {
|
||||
if (module(x, y) == runColor) {
|
||||
runY++;
|
||||
if (runY == 5)
|
||||
result += PENALTY_N1;
|
||||
else if (runY > 5)
|
||||
result++;
|
||||
} else {
|
||||
finderPenaltyAddHistory(runY, runHistory);
|
||||
if (!runColor)
|
||||
result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
||||
runColor = module(x, y);
|
||||
runY = 1;
|
||||
}
|
||||
}
|
||||
result += finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
|
||||
}
|
||||
|
||||
// 2*2 blocks of modules having same color
|
||||
for (int y = 0; y < size - 1; y++) {
|
||||
for (int x = 0; x < size - 1; x++) {
|
||||
bool color = module(x, y);
|
||||
if ( color == module(x + 1, y) &&
|
||||
color == module(x, y + 1) &&
|
||||
color == module(x + 1, y + 1))
|
||||
result += PENALTY_N2;
|
||||
}
|
||||
}
|
||||
|
||||
// Balance of dark and light modules
|
||||
int dark = 0;
|
||||
for (const vector<bool> &row : modules) {
|
||||
for (bool color : row) {
|
||||
if (color)
|
||||
dark++;
|
||||
}
|
||||
}
|
||||
int total = size * size; // Note that size is odd, so dark/total != 1/2
|
||||
// Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
|
||||
int k = static_cast<int>((std::abs(dark * 20L - total * 10L) + total - 1) / total) - 1;
|
||||
assert(0 <= k && k <= 9);
|
||||
result += k * PENALTY_N4;
|
||||
assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
vector<int> QrCode::getAlignmentPatternPositions() const {
|
||||
if (version == 1)
|
||||
return vector<int>();
|
||||
else {
|
||||
int numAlign = version / 7 + 2;
|
||||
int step = (version * 8 + numAlign * 3 + 5) / (numAlign * 4 - 4) * 2;
|
||||
vector<int> result;
|
||||
for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step)
|
||||
result.insert(result.begin(), pos);
|
||||
result.insert(result.begin(), 6);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int QrCode::getNumRawDataModules(int ver) {
|
||||
if (ver < MIN_VERSION || ver > MAX_VERSION)
|
||||
throw std::domain_error("Version number out of range");
|
||||
int result = (16 * ver + 128) * ver + 64;
|
||||
if (ver >= 2) {
|
||||
int numAlign = ver / 7 + 2;
|
||||
result -= (25 * numAlign - 10) * numAlign - 55;
|
||||
if (ver >= 7)
|
||||
result -= 36;
|
||||
}
|
||||
assert(208 <= result && result <= 29648);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
int QrCode::getNumDataCodewords(int ver, Ecc ecl) {
|
||||
return getNumRawDataModules(ver) / 8
|
||||
- ECC_CODEWORDS_PER_BLOCK [static_cast<int>(ecl)][ver]
|
||||
* NUM_ERROR_CORRECTION_BLOCKS[static_cast<int>(ecl)][ver];
|
||||
}
|
||||
|
||||
|
||||
vector<uint8_t> QrCode::reedSolomonComputeDivisor(int degree) {
|
||||
if (degree < 1 || degree > 255)
|
||||
throw std::domain_error("Degree out of range");
|
||||
// Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
|
||||
// For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}.
|
||||
vector<uint8_t> result(static_cast<size_t>(degree));
|
||||
result.at(result.size() - 1) = 1; // Start off with the monomial x^0
|
||||
|
||||
// Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
|
||||
// and drop the highest monomial term which is always 1x^degree.
|
||||
// Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
|
||||
uint8_t root = 1;
|
||||
for (int i = 0; i < degree; i++) {
|
||||
// Multiply the current product by (x - r^i)
|
||||
for (size_t j = 0; j < result.size(); j++) {
|
||||
result.at(j) = reedSolomonMultiply(result.at(j), root);
|
||||
if (j + 1 < result.size())
|
||||
result.at(j) ^= result.at(j + 1);
|
||||
}
|
||||
root = reedSolomonMultiply(root, 0x02);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
vector<uint8_t> QrCode::reedSolomonComputeRemainder(const vector<uint8_t> &data, const vector<uint8_t> &divisor) {
|
||||
vector<uint8_t> result(divisor.size());
|
||||
for (uint8_t b : data) { // Polynomial division
|
||||
uint8_t factor = b ^ result.at(0);
|
||||
result.erase(result.begin());
|
||||
result.push_back(0);
|
||||
for (size_t i = 0; i < result.size(); i++)
|
||||
result.at(i) ^= reedSolomonMultiply(divisor.at(i), factor);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
uint8_t QrCode::reedSolomonMultiply(uint8_t x, uint8_t y) {
|
||||
// Russian peasant multiplication
|
||||
int z = 0;
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
z = (z << 1) ^ ((z >> 7) * 0x11D);
|
||||
z ^= ((y >> i) & 1) * x;
|
||||
}
|
||||
assert(z >> 8 == 0);
|
||||
return static_cast<uint8_t>(z);
|
||||
}
|
||||
|
||||
|
||||
int QrCode::finderPenaltyCountPatterns(const std::array<int,7> &runHistory) const {
|
||||
int n = runHistory.at(1);
|
||||
assert(n <= size * 3);
|
||||
bool core = n > 0 && runHistory.at(2) == n && runHistory.at(3) == n * 3 && runHistory.at(4) == n && runHistory.at(5) == n;
|
||||
return (core && runHistory.at(0) >= n * 4 && runHistory.at(6) >= n ? 1 : 0)
|
||||
+ (core && runHistory.at(6) >= n * 4 && runHistory.at(0) >= n ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
int QrCode::finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array<int,7> &runHistory) const {
|
||||
if (currentRunColor) { // Terminate dark run
|
||||
finderPenaltyAddHistory(currentRunLength, runHistory);
|
||||
currentRunLength = 0;
|
||||
}
|
||||
currentRunLength += size; // Add light border to final run
|
||||
finderPenaltyAddHistory(currentRunLength, runHistory);
|
||||
return finderPenaltyCountPatterns(runHistory);
|
||||
}
|
||||
|
||||
|
||||
void QrCode::finderPenaltyAddHistory(int currentRunLength, std::array<int,7> &runHistory) const {
|
||||
if (runHistory.at(0) == 0)
|
||||
currentRunLength += size; // Add light border to initial run
|
||||
std::copy_backward(runHistory.cbegin(), runHistory.cend() - 1, runHistory.end());
|
||||
runHistory.at(0) = currentRunLength;
|
||||
}
|
||||
|
||||
|
||||
bool QrCode::getBit(long x, int i) {
|
||||
return ((x >> i) & 1) != 0;
|
||||
}
|
||||
|
||||
|
||||
/*---- Tables of constants ----*/
|
||||
|
||||
const int QrCode::PENALTY_N1 = 3;
|
||||
const int QrCode::PENALTY_N2 = 3;
|
||||
const int QrCode::PENALTY_N3 = 40;
|
||||
const int QrCode::PENALTY_N4 = 10;
|
||||
|
||||
|
||||
const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = {
|
||||
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
||||
//0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
|
||||
{-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low
|
||||
{-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium
|
||||
{-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile
|
||||
{-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High
|
||||
};
|
||||
|
||||
const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = {
|
||||
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
||||
//0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
|
||||
{-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low
|
||||
{-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium
|
||||
{-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile
|
||||
{-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High
|
||||
};
|
||||
|
||||
|
||||
data_too_long::data_too_long(const std::string &msg) :
|
||||
std::length_error(msg) {}
|
||||
|
||||
|
||||
|
||||
/*---- Class BitBuffer ----*/
|
||||
|
||||
BitBuffer::BitBuffer()
|
||||
: std::vector<bool>() {}
|
||||
|
||||
|
||||
void BitBuffer::appendBits(std::uint32_t val, int len) {
|
||||
if (len < 0 || len > 31 || val >> len != 0)
|
||||
throw std::domain_error("Value out of range");
|
||||
for (int i = len - 1; i >= 0; i--) // Append bit by bit
|
||||
this->push_back(((val >> i) & 1) != 0);
|
||||
}
|
||||
|
||||
}
|
||||
549
linux/thirdparty/QR-Code-generator/qrcodegen.hpp
vendored
Normal file
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
* QR Code generator library (C++)
|
||||
*
|
||||
* Copyright (c) Project Nayuki. (MIT License)
|
||||
* https://www.nayuki.io/page/qr-code-generator-library
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - The Software is provided "as is", without warranty of any kind, express or
|
||||
* implied, including but not limited to the warranties of merchantability,
|
||||
* fitness for a particular purpose and noninfringement. In no event shall the
|
||||
* authors or copyright holders be liable for any claim, damages or other
|
||||
* liability, whether in an action of contract, tort or otherwise, arising from,
|
||||
* out of or in connection with the Software or the use or other dealings in the
|
||||
* Software.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace qrcodegen {
|
||||
|
||||
/*
|
||||
* A segment of character/binary/control data in a QR Code symbol.
|
||||
* Instances of this class are immutable.
|
||||
* The mid-level way to create a segment is to take the payload data
|
||||
* and call a static factory function such as QrSegment::makeNumeric().
|
||||
* The low-level way to create a segment is to custom-make the bit buffer
|
||||
* and call the QrSegment() constructor with appropriate values.
|
||||
* This segment class imposes no length restrictions, but QR Codes have restrictions.
|
||||
* Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
|
||||
* Any segment longer than this is meaningless for the purpose of generating QR Codes.
|
||||
*/
|
||||
class QrSegment final {
|
||||
|
||||
/*---- Public helper enumeration ----*/
|
||||
|
||||
/*
|
||||
* Describes how a segment's data bits are interpreted. Immutable.
|
||||
*/
|
||||
public: class Mode final {
|
||||
|
||||
/*-- Constants --*/
|
||||
|
||||
public: static const Mode NUMERIC;
|
||||
public: static const Mode ALPHANUMERIC;
|
||||
public: static const Mode BYTE;
|
||||
public: static const Mode KANJI;
|
||||
public: static const Mode ECI;
|
||||
|
||||
|
||||
/*-- Fields --*/
|
||||
|
||||
// The mode indicator bits, which is a uint4 value (range 0 to 15).
|
||||
private: int modeBits;
|
||||
|
||||
// Number of character count bits for three different version ranges.
|
||||
private: int numBitsCharCount[3];
|
||||
|
||||
|
||||
/*-- Constructor --*/
|
||||
|
||||
private: Mode(int mode, int cc0, int cc1, int cc2);
|
||||
|
||||
|
||||
/*-- Methods --*/
|
||||
|
||||
/*
|
||||
* (Package-private) Returns the mode indicator bits, which is an unsigned 4-bit value (range 0 to 15).
|
||||
*/
|
||||
public: int getModeBits() const;
|
||||
|
||||
/*
|
||||
* (Package-private) Returns the bit width of the character count field for a segment in
|
||||
* this mode in a QR Code at the given version number. The result is in the range [0, 16].
|
||||
*/
|
||||
public: int numCharCountBits(int ver) const;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*---- Static factory functions (mid level) ----*/
|
||||
|
||||
/*
|
||||
* Returns a segment representing the given binary data encoded in
|
||||
* byte mode. All input byte vectors are acceptable. Any text string
|
||||
* can be converted to UTF-8 bytes and encoded as a byte mode segment.
|
||||
*/
|
||||
public: static QrSegment makeBytes(const std::vector<std::uint8_t> &data);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing the given string of decimal digits encoded in numeric mode.
|
||||
*/
|
||||
public: static QrSegment makeNumeric(const char *digits);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing the given text string encoded in alphanumeric mode.
|
||||
* The characters allowed are: 0 to 9, A to Z (uppercase only), space,
|
||||
* dollar, percent, asterisk, plus, hyphen, period, slash, colon.
|
||||
*/
|
||||
public: static QrSegment makeAlphanumeric(const char *text);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a list of zero or more segments to represent the given text string. The result
|
||||
* may use various segment modes and switch modes to optimize the length of the bit stream.
|
||||
*/
|
||||
public: static std::vector<QrSegment> makeSegments(const char *text);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing an Extended Channel Interpretation
|
||||
* (ECI) designator with the given assignment value.
|
||||
*/
|
||||
public: static QrSegment makeEci(long assignVal);
|
||||
|
||||
|
||||
/*---- Public static helper functions ----*/
|
||||
|
||||
/*
|
||||
* Tests whether the given string can be encoded as a segment in numeric mode.
|
||||
* A string is encodable iff each character is in the range 0 to 9.
|
||||
*/
|
||||
public: static bool isNumeric(const char *text);
|
||||
|
||||
|
||||
/*
|
||||
* Tests whether the given string can be encoded as a segment in alphanumeric mode.
|
||||
* A string is encodable iff each character is in the following set: 0 to 9, A to Z
|
||||
* (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
|
||||
*/
|
||||
public: static bool isAlphanumeric(const char *text);
|
||||
|
||||
|
||||
|
||||
/*---- Instance fields ----*/
|
||||
|
||||
/* The mode indicator of this segment. Accessed through getMode(). */
|
||||
private: const Mode *mode;
|
||||
|
||||
/* The length of this segment's unencoded data. Measured in characters for
|
||||
* numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
|
||||
* Always zero or positive. Not the same as the data's bit length.
|
||||
* Accessed through getNumChars(). */
|
||||
private: int numChars;
|
||||
|
||||
/* The data bits of this segment. Accessed through getData(). */
|
||||
private: std::vector<bool> data;
|
||||
|
||||
|
||||
/*---- Constructors (low level) ----*/
|
||||
|
||||
/*
|
||||
* Creates a new QR Code segment with the given attributes and data.
|
||||
* The character count (numCh) must agree with the mode and the bit buffer length,
|
||||
* but the constraint isn't checked. The given bit buffer is copied and stored.
|
||||
*/
|
||||
public: QrSegment(const Mode &md, int numCh, const std::vector<bool> &dt);
|
||||
|
||||
|
||||
/*
|
||||
* Creates a new QR Code segment with the given parameters and data.
|
||||
* The character count (numCh) must agree with the mode and the bit buffer length,
|
||||
* but the constraint isn't checked. The given bit buffer is moved and stored.
|
||||
*/
|
||||
public: QrSegment(const Mode &md, int numCh, std::vector<bool> &&dt);
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/*
|
||||
* Returns the mode field of this segment.
|
||||
*/
|
||||
public: const Mode &getMode() const;
|
||||
|
||||
|
||||
/*
|
||||
* Returns the character count field of this segment.
|
||||
*/
|
||||
public: int getNumChars() const;
|
||||
|
||||
|
||||
/*
|
||||
* Returns the data bits of this segment.
|
||||
*/
|
||||
public: const std::vector<bool> &getData() const;
|
||||
|
||||
|
||||
// (Package-private) Calculates the number of bits needed to encode the given segments at
|
||||
// the given version. Returns a non-negative number if successful. Otherwise returns -1 if a
|
||||
// segment has too many characters to fit its length field, or the total bits exceeds INT_MAX.
|
||||
public: static int getTotalBits(const std::vector<QrSegment> &segs, int version);
|
||||
|
||||
|
||||
/*---- Private constant ----*/
|
||||
|
||||
/* The set of all legal characters in alphanumeric mode, where
|
||||
* each character value maps to the index in the string. */
|
||||
private: static const char *ALPHANUMERIC_CHARSET;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* A QR Code symbol, which is a type of two-dimension barcode.
|
||||
* Invented by Denso Wave and described in the ISO/IEC 18004 standard.
|
||||
* Instances of this class represent an immutable square grid of dark and light cells.
|
||||
* The class provides static factory functions to create a QR Code from text or binary data.
|
||||
* The class covers the QR Code Model 2 specification, supporting all versions (sizes)
|
||||
* from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
|
||||
*
|
||||
* Ways to create a QR Code object:
|
||||
* - High level: Take the payload data and call QrCode::encodeText() or QrCode::encodeBinary().
|
||||
* - Mid level: Custom-make the list of segments and call QrCode::encodeSegments().
|
||||
* - Low level: Custom-make the array of data codeword bytes (including
|
||||
* segment headers and final padding, excluding error correction codewords),
|
||||
* supply the appropriate version number, and call the QrCode() constructor.
|
||||
* (Note that all ways require supplying the desired error correction level.)
|
||||
*/
|
||||
class QrCode final {
|
||||
|
||||
/*---- Public helper enumeration ----*/
|
||||
|
||||
/*
|
||||
* The error correction level in a QR Code symbol.
|
||||
*/
|
||||
public: enum class Ecc {
|
||||
LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords
|
||||
MEDIUM , // The QR Code can tolerate about 15% erroneous codewords
|
||||
QUARTILE, // The QR Code can tolerate about 25% erroneous codewords
|
||||
HIGH , // The QR Code can tolerate about 30% erroneous codewords
|
||||
};
|
||||
|
||||
|
||||
// Returns a value in the range 0 to 3 (unsigned 2-bit integer).
|
||||
private: static int getFormatBits(Ecc ecl);
|
||||
|
||||
|
||||
|
||||
/*---- Static factory functions (high level) ----*/
|
||||
|
||||
/*
|
||||
* Returns a QR Code representing the given Unicode text string at the given error correction level.
|
||||
* As a conservative upper bound, this function is guaranteed to succeed for strings that have 2953 or fewer
|
||||
* UTF-8 code units (not Unicode code points) if the low error correction level is used. The smallest possible
|
||||
* QR Code version is automatically chosen for the output. The ECC level of the result may be higher than
|
||||
* the ecl argument if it can be done without increasing the version.
|
||||
*/
|
||||
public: static QrCode encodeText(const char *text, Ecc ecl);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a QR Code representing the given binary data at the given error correction level.
|
||||
* This function always encodes using the binary segment mode, not any text mode. The maximum number of
|
||||
* bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output.
|
||||
* The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version.
|
||||
*/
|
||||
public: static QrCode encodeBinary(const std::vector<std::uint8_t> &data, Ecc ecl);
|
||||
|
||||
|
||||
/*---- Static factory functions (mid level) ----*/
|
||||
|
||||
/*
|
||||
* Returns a QR Code representing the given segments with the given encoding parameters.
|
||||
* The smallest possible QR Code version within the given range is automatically
|
||||
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
|
||||
* may be higher than the ecl argument if it can be done without increasing the
|
||||
* version. The mask number is either between 0 to 7 (inclusive) to force that
|
||||
* mask, or -1 to automatically choose an appropriate mask (which may be slow).
|
||||
* This function allows the user to create a custom sequence of segments that switches
|
||||
* between modes (such as alphanumeric and byte) to encode text in less space.
|
||||
* This is a mid-level API; the high-level API is encodeText() and encodeBinary().
|
||||
*/
|
||||
public: static QrCode encodeSegments(const std::vector<QrSegment> &segs, Ecc ecl,
|
||||
int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters
|
||||
|
||||
|
||||
|
||||
/*---- Instance fields ----*/
|
||||
|
||||
// Immutable scalar parameters:
|
||||
|
||||
/* The version number of this QR Code, which is between 1 and 40 (inclusive).
|
||||
* This determines the size of this barcode. */
|
||||
private: int version;
|
||||
|
||||
/* The width and height of this QR Code, measured in modules, between
|
||||
* 21 and 177 (inclusive). This is equal to version * 4 + 17. */
|
||||
private: int size;
|
||||
|
||||
/* The error correction level used in this QR Code. */
|
||||
private: Ecc errorCorrectionLevel;
|
||||
|
||||
/* The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
|
||||
* Even if a QR Code is created with automatic masking requested (mask = -1),
|
||||
* the resulting object still has a mask value between 0 and 7. */
|
||||
private: int mask;
|
||||
|
||||
// Private grids of modules/pixels, with dimensions of size*size:
|
||||
|
||||
// The modules of this QR Code (false = light, true = dark).
|
||||
// Immutable after constructor finishes. Accessed through getModule().
|
||||
private: std::vector<std::vector<bool> > modules;
|
||||
|
||||
// Indicates function modules that are not subjected to masking. Discarded when constructor finishes.
|
||||
private: std::vector<std::vector<bool> > isFunction;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor (low level) ----*/
|
||||
|
||||
/*
|
||||
* Creates a new QR Code with the given version number,
|
||||
* error correction level, data codeword bytes, and mask number.
|
||||
* This is a low-level API that most users should not use directly.
|
||||
* A mid-level API is the encodeSegments() function.
|
||||
*/
|
||||
public: QrCode(int ver, Ecc ecl, const std::vector<std::uint8_t> &dataCodewords, int msk);
|
||||
|
||||
|
||||
|
||||
/*---- Public instance methods ----*/
|
||||
|
||||
/*
|
||||
* Returns this QR Code's version, in the range [1, 40].
|
||||
*/
|
||||
public: int getVersion() const;
|
||||
|
||||
|
||||
/*
|
||||
* Returns this QR Code's size, in the range [21, 177].
|
||||
*/
|
||||
public: int getSize() const;
|
||||
|
||||
|
||||
/*
|
||||
* Returns this QR Code's error correction level.
|
||||
*/
|
||||
public: Ecc getErrorCorrectionLevel() const;
|
||||
|
||||
|
||||
/*
|
||||
* Returns this QR Code's mask, in the range [0, 7].
|
||||
*/
|
||||
public: int getMask() const;
|
||||
|
||||
|
||||
/*
|
||||
* Returns the color of the module (pixel) at the given coordinates, which is false
|
||||
* for light or true for dark. The top left corner has the coordinates (x=0, y=0).
|
||||
* If the given coordinates are out of bounds, then false (light) is returned.
|
||||
*/
|
||||
public: bool getModule(int x, int y) const;
|
||||
|
||||
|
||||
|
||||
/*---- Private helper methods for constructor: Drawing function modules ----*/
|
||||
|
||||
// Reads this object's version field, and draws and marks all function modules.
|
||||
private: void drawFunctionPatterns();
|
||||
|
||||
|
||||
// Draws two copies of the format bits (with its own error correction code)
|
||||
// based on the given mask and this object's error correction level field.
|
||||
private: void drawFormatBits(int msk);
|
||||
|
||||
|
||||
// Draws two copies of the version bits (with its own error correction code),
|
||||
// based on this object's version field, iff 7 <= version <= 40.
|
||||
private: void drawVersion();
|
||||
|
||||
|
||||
// Draws a 9*9 finder pattern including the border separator,
|
||||
// with the center module at (x, y). Modules can be out of bounds.
|
||||
private: void drawFinderPattern(int x, int y);
|
||||
|
||||
|
||||
// Draws a 5*5 alignment pattern, with the center module
|
||||
// at (x, y). All modules must be in bounds.
|
||||
private: void drawAlignmentPattern(int x, int y);
|
||||
|
||||
|
||||
// Sets the color of a module and marks it as a function module.
|
||||
// Only used by the constructor. Coordinates must be in bounds.
|
||||
private: void setFunctionModule(int x, int y, bool isDark);
|
||||
|
||||
|
||||
// Returns the color of the module at the given coordinates, which must be in range.
|
||||
private: bool module(int x, int y) const;
|
||||
|
||||
|
||||
/*---- Private helper methods for constructor: Codewords and masking ----*/
|
||||
|
||||
// Returns a new byte string representing the given data with the appropriate error correction
|
||||
// codewords appended to it, based on this object's version and error correction level.
|
||||
private: std::vector<std::uint8_t> addEccAndInterleave(const std::vector<std::uint8_t> &data) const;
|
||||
|
||||
|
||||
// Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
|
||||
// data area of this QR Code. Function modules need to be marked off before this is called.
|
||||
private: void drawCodewords(const std::vector<std::uint8_t> &data);
|
||||
|
||||
|
||||
// XORs the codeword modules in this QR Code with the given mask pattern.
|
||||
// The function modules must be marked and the codeword bits must be drawn
|
||||
// before masking. Due to the arithmetic of XOR, calling applyMask() with
|
||||
// the same mask value a second time will undo the mask. A final well-formed
|
||||
// QR Code needs exactly one (not zero, two, etc.) mask applied.
|
||||
private: void applyMask(int msk);
|
||||
|
||||
|
||||
// Calculates and returns the penalty score based on state of this QR Code's current modules.
|
||||
// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
|
||||
private: long getPenaltyScore() const;
|
||||
|
||||
|
||||
|
||||
/*---- Private helper functions ----*/
|
||||
|
||||
// Returns an ascending list of positions of alignment patterns for this version number.
|
||||
// Each position is in the range [0,177), and are used on both the x and y axes.
|
||||
// This could be implemented as lookup table of 40 variable-length lists of unsigned bytes.
|
||||
private: std::vector<int> getAlignmentPatternPositions() const;
|
||||
|
||||
|
||||
// Returns the number of data bits that can be stored in a QR Code of the given version number, after
|
||||
// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
|
||||
// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
|
||||
private: static int getNumRawDataModules(int ver);
|
||||
|
||||
|
||||
// Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
|
||||
// QR Code of the given version number and error correction level, with remainder bits discarded.
|
||||
// This stateless pure function could be implemented as a (40*4)-cell lookup table.
|
||||
private: static int getNumDataCodewords(int ver, Ecc ecl);
|
||||
|
||||
|
||||
// Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be
|
||||
// implemented as a lookup table over all possible parameter values, instead of as an algorithm.
|
||||
private: static std::vector<std::uint8_t> reedSolomonComputeDivisor(int degree);
|
||||
|
||||
|
||||
// Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
|
||||
private: static std::vector<std::uint8_t> reedSolomonComputeRemainder(const std::vector<std::uint8_t> &data, const std::vector<std::uint8_t> &divisor);
|
||||
|
||||
|
||||
// Returns the product of the two given field elements modulo GF(2^8/0x11D).
|
||||
// All inputs are valid. This could be implemented as a 256*256 lookup table.
|
||||
private: static std::uint8_t reedSolomonMultiply(std::uint8_t x, std::uint8_t y);
|
||||
|
||||
|
||||
// Can only be called immediately after a light run is added, and
|
||||
// returns either 0, 1, or 2. A helper function for getPenaltyScore().
|
||||
private: int finderPenaltyCountPatterns(const std::array<int,7> &runHistory) const;
|
||||
|
||||
|
||||
// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
|
||||
private: int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array<int,7> &runHistory) const;
|
||||
|
||||
|
||||
// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
|
||||
private: void finderPenaltyAddHistory(int currentRunLength, std::array<int,7> &runHistory) const;
|
||||
|
||||
|
||||
// Returns true iff the i'th bit of x is set to 1.
|
||||
private: static bool getBit(long x, int i);
|
||||
|
||||
|
||||
/*---- Constants and tables ----*/
|
||||
|
||||
// The minimum version number supported in the QR Code Model 2 standard.
|
||||
public: static constexpr int MIN_VERSION = 1;
|
||||
|
||||
// The maximum version number supported in the QR Code Model 2 standard.
|
||||
public: static constexpr int MAX_VERSION = 40;
|
||||
|
||||
|
||||
// For use in getPenaltyScore(), when evaluating which mask is best.
|
||||
private: static const int PENALTY_N1;
|
||||
private: static const int PENALTY_N2;
|
||||
private: static const int PENALTY_N3;
|
||||
private: static const int PENALTY_N4;
|
||||
|
||||
|
||||
private: static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41];
|
||||
private: static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41];
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*---- Public exception class ----*/
|
||||
|
||||
/*
|
||||
* Thrown when the supplied data does not fit any QR Code version. Ways to handle this exception include:
|
||||
* - Decrease the error correction level if it was greater than Ecc::LOW.
|
||||
* - If the encodeSegments() function was called with a maxVersion argument, then increase
|
||||
* it if it was less than QrCode::MAX_VERSION. (This advice does not apply to the other
|
||||
* factory functions because they search all versions up to QrCode::MAX_VERSION.)
|
||||
* - Split the text data into better or optimal segments in order to reduce the number of bits required.
|
||||
* - Change the text or binary data to be shorter.
|
||||
* - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric).
|
||||
* - Propagate the error upward to the caller/user.
|
||||
*/
|
||||
class data_too_long : public std::length_error {
|
||||
|
||||
public: explicit data_too_long(const std::string &msg);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* An appendable sequence of bits (0s and 1s). Mainly used by QrSegment.
|
||||
*/
|
||||
class BitBuffer final : public std::vector<bool> {
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
// Creates an empty bit buffer (length 0).
|
||||
public: BitBuffer();
|
||||
|
||||
|
||||
|
||||
/*---- Method ----*/
|
||||
|
||||
// Appends the given number of low-order bits of the given value
|
||||
// to this buffer. Requires 0 <= len <= 31 and val < 2^len.
|
||||
public: void appendBits(std::uint32_t val, int len);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
214
proximity_keys.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Needs https://github.com/google/bumble on Windows
|
||||
# See https://github.com/google/bumble/blob/main/docs/mkdocs/src/platforms/windows.md for usage.
|
||||
# You need to associate WinUSB with your Bluetooth interface. Once done, you can roll back to the original driver from Device Manager.
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
import logging
|
||||
import platform
|
||||
from typing import Any, Optional
|
||||
|
||||
from colorama import Fore, Style, init as colorama_init
|
||||
colorama_init(autoreset=True)
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
class ColorFormatter(logging.Formatter):
|
||||
COLORS = {
|
||||
logging.DEBUG: Fore.BLUE,
|
||||
logging.INFO: Fore.GREEN,
|
||||
logging.WARNING: Fore.YELLOW,
|
||||
logging.ERROR: Fore.RED,
|
||||
logging.CRITICAL: Fore.MAGENTA,
|
||||
}
|
||||
def format(self, record):
|
||||
color = self.COLORS.get(record.levelno, "")
|
||||
prefix = f"{color}[{record.levelname}:{record.name}]{Style.RESET_ALL}"
|
||||
return f"{prefix} {record.getMessage()}"
|
||||
handler.setFormatter(ColorFormatter())
|
||||
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
||||
logger = logging.getLogger("proximitykeys")
|
||||
|
||||
PROXIMITY_KEY_TYPES = {0x01: "IRK", 0x04: "ENC_KEY"}
|
||||
|
||||
def parse_proximity_keys_response(data: bytes):
|
||||
if len(data) < 7 or data[4] != 0x31:
|
||||
return None
|
||||
key_count = data[6]
|
||||
keys = []
|
||||
offset = 7
|
||||
for _ in range(key_count):
|
||||
if offset + 3 >= len(data):
|
||||
break
|
||||
key_type = data[offset]
|
||||
key_length = data[offset + 2]
|
||||
offset += 4
|
||||
if offset + key_length > len(data):
|
||||
break
|
||||
key_bytes = data[offset:offset + key_length]
|
||||
keys.append((PROXIMITY_KEY_TYPES.get(key_type, f"TYPE_{key_type:02X}"), key_bytes))
|
||||
offset += key_length
|
||||
return keys
|
||||
|
||||
def hexdump(data: bytes) -> str:
|
||||
return " ".join(f"{b:02X}" for b in data)
|
||||
|
||||
async def run_bumble(bdaddr: str):
|
||||
try:
|
||||
from bumble.l2cap import ClassicChannelSpec
|
||||
from bumble.transport import open_transport
|
||||
from bumble.device import Device
|
||||
from bumble.host import Host
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
from bumble.hci import HCI_Error
|
||||
except ImportError:
|
||||
logger.error("Bumble not installed")
|
||||
return 1
|
||||
|
||||
PSM_PROXIMITY = 0x1001
|
||||
HANDSHAKE = bytes.fromhex("00 00 04 00 01 00 02 00 00 00 00 00 00 00 00 00")
|
||||
KEY_REQ = bytes.fromhex("04 00 04 00 30 00 05 00")
|
||||
|
||||
class KeyStore:
|
||||
async def delete(self, name: str): pass
|
||||
async def update(self, name: str, keys: Any): pass
|
||||
async def get(self, _name: str) -> Optional[Any]: return None
|
||||
async def get_all(self): return []
|
||||
|
||||
async def get_resolving_keys(self) -> list[tuple[bytes, Any]]:
|
||||
all_keys = await self.get_all()
|
||||
resolving_keys = []
|
||||
for name, keys in all_keys:
|
||||
if getattr(keys, "irk", None) is not None:
|
||||
resolving_keys.append((
|
||||
keys.irk.value,
|
||||
getattr(keys, "address", "UNKNOWN")
|
||||
))
|
||||
return resolving_keys
|
||||
|
||||
async def exchange_keys(channel, timeout=5.0):
|
||||
recv_q: asyncio.Queue = asyncio.Queue()
|
||||
channel.sink = lambda sdu: recv_q.put_nowait(sdu)
|
||||
logger.info("Sending handshake packet...")
|
||||
channel.send_pdu(HANDSHAKE)
|
||||
await asyncio.sleep(0.5)
|
||||
logger.info("Sending key request packet...")
|
||||
channel.send_pdu(KEY_REQ)
|
||||
while True:
|
||||
try:
|
||||
pkt = await asyncio.wait_for(recv_q.get(), timeout)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Timed out waiting for SDU response")
|
||||
return None
|
||||
logger.debug("Received SDU (%d bytes): %s", len(pkt), hexdump(pkt))
|
||||
keys = parse_proximity_keys_response(pkt)
|
||||
if keys:
|
||||
return keys
|
||||
|
||||
async def get_device():
|
||||
logger.info("Opening transport...")
|
||||
transport = await open_transport("usb:0")
|
||||
device = Device(host=Host(controller_source=transport.source, controller_sink=transport.sink))
|
||||
device.classic_enabled = True
|
||||
device.le_enabled = False
|
||||
device.keystore = KeyStore()
|
||||
device.pairing_config_factory = lambda conn: PairingConfig(
|
||||
sc=True, mitm=False, bonding=True,
|
||||
delegate=PairingDelegate(io_capability=PairingDelegate.NO_OUTPUT_NO_INPUT)
|
||||
)
|
||||
await device.power_on()
|
||||
logger.info("Device powered on")
|
||||
return transport, device
|
||||
|
||||
async def create_channel_and_exchange(conn):
|
||||
spec = ClassicChannelSpec(psm=PSM_PROXIMITY, mtu=2048)
|
||||
logger.info("Requesting L2CAP channel on PSM = 0x%04X", spec.psm)
|
||||
if not conn.is_encrypted:
|
||||
logger.info("Enabling link encryption...")
|
||||
await conn.encrypt()
|
||||
await asyncio.sleep(0.05)
|
||||
channel = await conn.create_l2cap_channel(spec=spec)
|
||||
keys = await exchange_keys(channel, timeout=8.0)
|
||||
if not keys:
|
||||
logger.warning("No proximity keys found")
|
||||
return
|
||||
logger.info("Keys successfully retrieved")
|
||||
print(f"{Fore.CYAN}{Style.BRIGHT}Proximity Keys:{Style.RESET_ALL}")
|
||||
for name, key_bytes in keys:
|
||||
print(f" {Fore.MAGENTA}{name}{Style.RESET_ALL}: {hexdump(key_bytes)}")
|
||||
|
||||
transport, device = await get_device()
|
||||
logger.info("Connecting to %s (BR/EDR)...", bdaddr)
|
||||
try:
|
||||
connection = await device.connect(bdaddr, PhysicalTransport.BR_EDR)
|
||||
logger.info("Connected to %s (handle %s)", connection.peer_address, connection.handle)
|
||||
logger.info("Authenticating...")
|
||||
await connection.authenticate()
|
||||
if not connection.is_encrypted:
|
||||
logger.info("Encrypting link...")
|
||||
await connection.encrypt()
|
||||
await create_channel_and_exchange(connection)
|
||||
except HCI_Error as e:
|
||||
if "PAIRING_NOT_ALLOWED_ERROR" in str(e):
|
||||
logger.error("Put your device into pairing mode and run the script again")
|
||||
else:
|
||||
logger.error("HCI error: %s", e)
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error: %s", e)
|
||||
finally:
|
||||
if hasattr(transport, "close"):
|
||||
logger.info("Closing transport...")
|
||||
await transport.close()
|
||||
logger.info("Transport closed")
|
||||
return 0
|
||||
|
||||
def run_linux(bdaddr: str):
|
||||
import socket
|
||||
PSM = 0x1001
|
||||
handshake = bytes.fromhex("00 00 04 00 01 00 02 00 00 00 00 00 00 00 00 00")
|
||||
key_req = bytes.fromhex("04 00 04 00 30 00 05 00")
|
||||
|
||||
logger.info("Connecting to %s (L2CAP)...", bdaddr)
|
||||
sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
|
||||
try:
|
||||
sock.connect((bdaddr, PSM))
|
||||
logger.info("Connected, sending handshake and key request...")
|
||||
sock.send(handshake)
|
||||
sock.send(key_req)
|
||||
|
||||
while True:
|
||||
pkt = sock.recv(1024)
|
||||
logger.debug("Received packet (%d bytes): %s", len(pkt), hexdump(pkt))
|
||||
keys = parse_proximity_keys_response(pkt)
|
||||
if keys:
|
||||
logger.info("Keys successfully retrieved")
|
||||
print(f"{Fore.CYAN}{Style.BRIGHT}Proximity Keys:{Style.RESET_ALL}")
|
||||
for name, key_bytes in keys:
|
||||
print(f" {Fore.MAGENTA}{name}{Style.RESET_ALL}: {hexdump(key_bytes)}")
|
||||
break
|
||||
else:
|
||||
logger.warning("Received packet did not contain keys, waiting...")
|
||||
except Exception as e:
|
||||
logger.error("Error during L2CAP exchange: %s", e)
|
||||
finally:
|
||||
sock.close()
|
||||
logger.info("Connection closed")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("bdaddr")
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
parser.add_argument("--bumble", action="store_true")
|
||||
args = parser.parse_args()
|
||||
logging.getLogger().setLevel(logging.DEBUG if args.debug else logging.INFO)
|
||||
|
||||
if args.bumble or platform.system() == "Windows":
|
||||
asyncio.run(run_bumble(args.bdaddr))
|
||||
else:
|
||||
run_linux(args.bdaddr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||