a few small changes
2
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- name: Export APK_NAME for later use
|
- name: Export APK_NAME for later use
|
||||||
run: echo "APK_NAME=ALN-$(echo ${{ github.sha }} | cut -c1-7).apk" >> $GITHUB_ENV
|
run: echo "APK_NAME=LibrePods-$(echo ${{ github.sha }} | cut -c1-7).apk" >> $GITHUB_ENV
|
||||||
- name: Rename .apk file
|
- name: Rename .apk file
|
||||||
run: mv "./Debug APK/debug/"*.apk "./$APK_NAME"
|
run: mv "./Debug APK/debug/"*.apk "./$APK_NAME"
|
||||||
- name: Decode keystore file
|
- name: Decode keystore file
|
||||||
|
|||||||
@@ -429,8 +429,8 @@ Once tracking is active, the AirPods stream sensor packets with the following co
|
|||||||
|
|
||||||
# LICENSE
|
# LICENSE
|
||||||
|
|
||||||
AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
Copyright (C) 2024 Kavish Devar
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## btl2capfix v0.0.3
|
## btl2capfix v0.0.3
|
||||||
- ([#34](https://github.com/kavishdevar/aln/pull/34)) @devnoname120 Add on-device libbluetooth patcher using a Magisk/KernelSU module (arm64-only)
|
- ([#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/aln/releases)_
|
_[See more here](https://github.com/kavishdevar/librepods/releases)_
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Welcome to AirPods Like Normal contributing guide <!-- omit in toc -->
|
# Welcome to LibrePods contributing guide <!-- omit in toc -->
|
||||||
|
|
||||||
Thank you for considering a contribution to AirPods Like Normal! Your support helps bring Apple-exclusive AirPods features to Linux and Android.
|
Thank you for considering a contribution to LibrePods! Your support helps bring Apple-exclusive AirPods features to Linux and Android.
|
||||||
|
|
||||||
Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectful.
|
Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectful.
|
||||||
|
|
||||||
@@ -25,11 +25,11 @@ To develop for the Android App, Android Studio is the preferred IDE. And you can
|
|||||||
|
|
||||||
#### Create a new issue
|
#### Create a new issue
|
||||||
|
|
||||||
If you find a bug or want to suggest a feature, check if an issue already exists by searching through our [existing issues](https://github.com/kavishdevar/aln/issues). If no relevant issue exists, open a new one and fill in the details.
|
If you find a bug or want to suggest a feature, check if an issue already exists by searching through our [existing issues](https://github.com/kavishdevar/librepods/issues). If no relevant issue exists, open a new one and fill in the details.
|
||||||
|
|
||||||
#### Solve an issue
|
#### Solve an issue
|
||||||
|
|
||||||
Browse our [issues list](https://github.com/kavishdevar/aln/issues) to find an interesting issue to work on. Use labels to filter issues and pick one that matches your expertise. If you’d like to work on an issue, open a PR with your solution.
|
Browse our [issues list](https://github.com/kavishdevar/librepods/issues) to find an interesting issue to work on. Use labels to filter issues and pick one that matches your expertise. If you’d like to work on an issue, open a PR with your solution.
|
||||||
|
|
||||||
### Make Changes
|
### Make Changes
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ Browse our [issues list](https://github.com/kavishdevar/aln/issues) to find an i
|
|||||||
|
|
||||||
1. Fork the repository and clone it to your local environment.
|
1. Fork the repository and clone it to your local environment.
|
||||||
```
|
```
|
||||||
git clone https://github.com/kavishdevar/aln.git
|
git clone https://github.com/kavishdevar/librepods.git
|
||||||
cd AirPods-Like-Normal
|
cd AirPods-Like-Normal
|
||||||
```
|
```
|
||||||
2. Create a working branch to start your changes.
|
2. Create a working branch to start your changes.
|
||||||
@@ -67,4 +67,4 @@ Once your PR is open, a team member will review it. They may ask questions or re
|
|||||||
|
|
||||||
### Your PR is merged!
|
### Your PR is merged!
|
||||||
|
|
||||||
Congratulations! :tada: Once merged, your contributions will be publicly available in AirPodsLikeNormal.
|
Congratulations! :tada: Once merged, your contributions will be publicly available in LibrePods.
|
||||||
136
README.md
@@ -1,47 +1,58 @@
|
|||||||
# ALN - AirPodsLikeNormal
|
# LibrePods
|
||||||
*Bringing AirPods' Apple-exclusive features on linux and android!*
|
|
||||||
|
|
||||||
## [XDAForums Thread](https://xdaforums.com/t/app-root-for-now-airpodslikenormal-unlock-apple-exclusive-airpods-features-on-android.4707585/)
|

|
||||||
|
|
||||||
## Tested device(s)
|
*AirPods liberated from Apple's ecosystem*
|
||||||
- AirPods Pro 2
|
|
||||||
|
|
||||||
Other devices might work too. Features like ear detection and battery should be available for any AirPods! Although the app will show unsupported features/settings. I will not be able test any other devices than the ones I already have (i.e. the AirPods Pro 2).
|
[](https://xdaforums.com/t/app-root-for-now-airpodslikenormal-unlock-apple-exclusive-airpods-features-on-android.4707585/)
|
||||||
|
|
||||||
## Features
|
## What is LibrePods?
|
||||||
|
|
||||||
Check the [pinned issue](https://github.com/kavishdevar/aln/issues/20) for a list.
|
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.
|
||||||
|
|
||||||
|
## Device Compatibility
|
||||||
|
|
||||||
## CrossDevice Stuff
|
| Status | Device | Features |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| ✅ | AirPods Pro (2nd Gen) | Fully supported and tested |
|
||||||
|
| ⚠️ | Other AirPods models | Basic features (battery status, ear detection) should work |
|
||||||
|
|
||||||
> [!IMPORTANT]
|
Most features should work with any AirPods. Currently, testing is only performed with AirPods Pro 2.
|
||||||
> This feature is still in development and might not work as expected. No support is provided for this feature.
|
|
||||||
|
|
||||||
### Features
|
## Key Features
|
||||||
|
|
||||||
- **Battery Status**: Get battery status on any device when you connect your AirPods to one of them.
|
- **Noise Control Modes**: Easily switch between noise control modes without having to reach out to your AirPods to long press
|
||||||
- **Control AirPods**: Control your AirPods from either of your device when you connect to one, like changing the noise control mode, toggling conversational awareness, and more.
|
- **Ear Detection**: Controls your music automatically when you put your AirPods in or take them out, and switch to phone speaker when you take them out
|
||||||
- **Automatic Device Switching**: Automatically switch between your Linux and Android device, like when you receive a call, start playing music on Android while you're connected to Linux, and more!
|
- **Battery Status**: Accurate battery levels
|
||||||
|
- **Head Gestures**: Answer calls just by nodding your head
|
||||||
|
- **Conversational Awareness**: Volume automatically lowers when you speak
|
||||||
|
- **Other customizations**:
|
||||||
|
- Rename your AirPods
|
||||||
|
- Customize long-press actions
|
||||||
|
- Few accessibility features
|
||||||
|
- And more!
|
||||||
|
|
||||||
Check out the demo below!
|
See our [pinned issue](https://github.com/kavishdevar/librepods/issues/20) for a complete feature list and roadmap.
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/d08f8a51-cd52-458b-8e55-9b44f4d5f3ab
|
## Platform Support
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
## Linux
|
The Linux version runs as a system tray app. Connect your AirPods and enjoy:
|
||||||
|
|
||||||
The Linux version is a simple tray-app, with a modern and adaptive ui. Still WIP, but most things work (battery, ear-detection, auto-pause, rename, etc.)
|
- Battery monitoring
|
||||||
|
- Ear detection with auto-pause
|
||||||
|
- Device renaming
|
||||||
|
- Media controls
|
||||||
|
|
||||||
Check out the README file in [linux](/linux) folder for more info.
|
> [!NOTE]
|
||||||
|
> Work in progress, but core functionality is stable and usable.
|
||||||
|
|
||||||
## Android
|
For installation and detailed info, see the [Linux README](/linux/README.md).
|
||||||
|
|
||||||
> Can I use aln without root?
|
### Android
|
||||||
|
|
||||||
**No, it's not possible to use aln without root.** You will have to root your device if you want to use aln, and there is no way around it. **No exceptions.**
|
#### Screenshots
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
| | | |
|
| | | |
|
||||||
|-------------------|-------------------|-------------------|
|
|-------------------|-------------------|-------------------|
|
||||||
@@ -50,46 +61,75 @@ Check out the README file in [linux](/linux) folder for more info.
|
|||||||
|  |  |  |
|
|  |  |  |
|
||||||
|  | | |
|
|  | | |
|
||||||
|
|
||||||
### Installation
|
#### Root Requirement
|
||||||
|
|
||||||
Currently, there's a [bug in the Android Bluetooth stack](https://issuetracker.google.com/issues/371713238) that prevents the app from working (upvote the issue - click the '+1' icon on the top right corner of IssueTracker).
|
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Until Google merges the fix **you will only be able to use aln if you are rooted**. There are **no exceptions**, don't ask about it.
|
> **You must have a rooted device to use LibrePods on Android.** This is due to a [bug in the Android Bluetooth stack](https://issuetracker.google.com/issues/371713238). Please upvote the issue by clicking the '+1' icon on the IssueTracker page.
|
||||||
|
>
|
||||||
|
> There are **no exceptions** to the root requirement until Google merges the fix.
|
||||||
|
|
||||||
In order to use aln you will have to install the module using your favorite root manager in OverlayFS mode (KernelSU, Apatch, or Magisk). If you don't know what this means, no support is provided: you will have to search by yourself on Google or ask in some Android rooting communities on Telegram.
|
#### Installation Methods
|
||||||
|
|
||||||
The module to install is available in the releases section under the name `btl2capfix.zip`.
|
##### Method 1: Xposed Module (Recommended)
|
||||||
|
This method is less intrusive and should be tried first:
|
||||||
|
|
||||||
### Android – features
|
1. Install LSPosed, or another Xposed provider on your rooted device
|
||||||
|
2. Download the LibrePods app from the releases section, and install it.
|
||||||
|
3. Enable the Xposed module for the bluetooth app in your Xposed manager
|
||||||
|
4. Follow the instructions in the app to set up the module.
|
||||||
|
5. Open the app and connect your AirPods
|
||||||
|
|
||||||
#### Renaming the Airpods
|
##### Method 2: Root Module (Backup Option)
|
||||||
When you rename the Airpods using the app, you'll need to re-pair it with your phone. Currently, user-level apps cannot directly rename a Bluetooth device. After re-pairing, your phone will display the updated name!
|
If the Xposed method doesn't work for you:
|
||||||
|
|
||||||
#### Noise Control Modes
|
1. Download the `btl2capfix.zip` module from the releases section
|
||||||
|
2. Install it using your preferred root manager (KernelSU, Apatch, or Magisk).
|
||||||
|
3. Reboot your device
|
||||||
|
4. Connect your AirPods
|
||||||
|
|
||||||
- Active Noise Cancellation (ANC): Blocks external sounds using microphones and advanced algorithms for an immersive audio experience; ideal for noisy environments.
|
##### Method 3: Patching it yourself
|
||||||
- Transparency Mode: Allows external sounds to blend with audio for situational awareness; best for environments where you need to stay alert.
|
If you prefer to patch the Bluetooth stack yourself, follow these steps:
|
||||||
- Off Mode: Disables noise control for a natural listening experience, conserving battery in quiet settings.
|
|
||||||
- Adaptive Transparency: Dynamically reduces sudden loud noises while maintaining environmental awareness, adjusting seamlessly to fluctuating noise levels.
|
1. Look for the library in use by running `lsof | grep libbluetooth`
|
||||||
|
2. Find the library path (e.g., `/system/lib64/libbluetooth_jni.so`)
|
||||||
|
3. Find the `l2c_fcr_chk_chan_modes` function in the library
|
||||||
|
4. Patch the function to always return `1` (true)
|
||||||
|
5. Repack the library and push it back to the device. You can do this by creating a root module yourself.
|
||||||
|
6. Reboot your device
|
||||||
|
|
||||||
|
If you're unfamiliar with these steps, search for tutorials online or ask in Android rooting communities.
|
||||||
|
|
||||||
|
#### A few notes
|
||||||
|
|
||||||
|
- Due to recent AirPods' firmware upgrades, you must enable `Off listening mode` to switch to `Off`. This is because in this mode, louds sounds are not reduced!
|
||||||
|
|
||||||
|
- If you have take both AirPods out, the app will automatically switch to the phone speaker. But, Android might keep on trying to connect to the AirPods because the phone is still connected to them, just the A2DP profile is not connected. The app tries to disconnect the A2DP profile as soon as it detects that Android has connected again if they're not in the ear.
|
||||||
|
|
||||||
|
- When renaming your AirPods through the app, you'll need to re-pair them with your phone for the name change to take effect. This is a limitation of how Bluetooth device naming works on Android.
|
||||||
|
|
||||||
|
## Development Resources
|
||||||
|
|
||||||
|
For developers interested in the protocol details, check out the [AAP Definitions](/AAP%20Definitions.md) documentation.
|
||||||
|
|
||||||
|
## CrossDevice Stuff
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Due to recent AirPods' firmware upgrades, you must enable `Off listening mode` to switch to `Off`. This is because in this mode, louds sounds are not reduced!
|
> This feature is still in early development and might not work as expected. No support is provided for this feature yet.
|
||||||
|
|
||||||
#### Conversational Awareness
|
### Features in Development
|
||||||
|
|
||||||
Automatically lowers audio volume and enhances voices when you start speaking, making it easier to engage in conversations without removing your AirPods.
|
- **Battery Status Sync**: Get battery status on any device when you connect your AirPods to one of them
|
||||||
|
- **Cross-device Controls**: Control your AirPods from either device when connected to one
|
||||||
|
- **Automatic Device Switching**: Seamlessly switch between Linux and Android devices based on active audio sources
|
||||||
|
|
||||||
#### Automatic Ear Detection
|
Check out the demo below:
|
||||||
|
|
||||||
Recognizes when the AirPods are in your ears to automatically play or pause audio and adjust functionality accordingly.
|
https://github.com/user-attachments/assets/d08f8a51-cd52-458b-8e55-9b44f4d5f3ab
|
||||||
|
|
||||||
## Check out the packet definitions at [AAP Definitions](/AAP%20Definitions.md)
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
AirPodsLikeNormal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
Copyright (C) 2024 Kavish Devar
|
Copyright (C) 2025 LibrePods contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
|||||||
@@ -6,21 +6,15 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "me.kavishdevar.aln"
|
namespace = "me.kavishdevar.librepods"
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.kavishdevar.aln"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 3
|
versionCode = 4
|
||||||
versionName = "0.0.3"
|
versionName = "0.1.0"
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
cppFlags += ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ALN"
|
android:theme="@style/Theme.LibrePods"
|
||||||
tools:ignore="UnusedAttribute"
|
tools:ignore="UnusedAttribute"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<receiver
|
<receiver
|
||||||
@@ -67,7 +69,7 @@
|
|||||||
android:name=".CustomDevice"
|
android:name=".CustomDevice"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/title_activity_custom_device"
|
android:label="@string/title_activity_custom_device"
|
||||||
android:theme="@style/Theme.ALN">
|
android:theme="@style/Theme.LibrePods">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.ALN">
|
android:theme="@style/Theme.LibrePods">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -83,6 +85,15 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".QuickSettingsDialogActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.TransparentDialog"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:taskAffinity=""
|
||||||
|
/>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.AirPodsService"
|
android:name=".services.AirPodsService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -30,12 +30,21 @@
|
|||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
static HookFunType hook_func = nullptr;
|
static HookFunType hook_func = nullptr;
|
||||||
|
#define L2CEVT_L2CAP_CONFIG_REQ 4
|
||||||
static uint8_t (*original_l2c_fcr_chk_chan_modes)(void* p_ccb) = nullptr;
|
#define L2CEVT_L2CAP_CONFIG_RSP 15
|
||||||
|
|
||||||
// Define all necessary structures for the L2CAP stack
|
// Define all necessary structures for the L2CAP stack
|
||||||
|
|
||||||
// Define base FCR structure
|
// Forward declarations for types needed by the new hook
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Define base FCR structures
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t mode;
|
uint8_t mode;
|
||||||
uint8_t tx_win_sz;
|
uint8_t tx_win_sz;
|
||||||
@@ -89,50 +98,44 @@ typedef struct t_l2c_ccb {
|
|||||||
struct t_l2c_ccb* p_prev_ccb; // Previous 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_lcb* p_lcb; // Link this CCB belongs to
|
||||||
struct t_l2c_rcb* p_rcb; // Registration CB for this Channel
|
struct t_l2c_rcb* p_rcb; // Registration CB for this Channel
|
||||||
|
|
||||||
uint16_t local_cid; // Local CID
|
uint16_t local_cid; // Local CID
|
||||||
uint16_t remote_cid; // Remote CID
|
uint16_t remote_cid; // Remote CID
|
||||||
uint16_t p_lcb_next; // For linking CCBs to an LCB
|
uint16_t p_lcb_next; // For linking CCBs to an LCB
|
||||||
|
|
||||||
uint8_t ccb_priority; // Channel priority
|
uint8_t ccb_priority; // Channel priority
|
||||||
uint16_t tx_mps; // MPS for outgoing messages
|
uint16_t tx_mps; // MPS for outgoing messages
|
||||||
uint16_t max_rx_mtu; // Max MTU we will receive
|
uint16_t max_rx_mtu; // Max MTU we will receive
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
bool in_use; // True when channel active
|
bool in_use; // True when channel active
|
||||||
uint8_t chnl_state; // Channel state
|
uint8_t chnl_state; // Channel state
|
||||||
uint8_t local_id; // Transaction ID for local trans
|
uint8_t local_id; // Transaction ID for local trans
|
||||||
uint8_t remote_id; // Transaction ID for remote
|
uint8_t remote_id; // Transaction ID for remote
|
||||||
|
|
||||||
uint8_t timer_entry; // Timer entry
|
uint8_t timer_entry; // Timer entry
|
||||||
uint8_t is_flushable; // True if flushable
|
uint8_t is_flushable; // True if flushable
|
||||||
|
|
||||||
// Configuration variables
|
// Configuration variables
|
||||||
uint16_t our_cfg_bits; // Bitmap of local config bits
|
uint16_t our_cfg_bits; // Bitmap of local config bits
|
||||||
uint16_t peer_cfg_bits; // Bitmap of peer config bits
|
uint16_t peer_cfg_bits; // Bitmap of peer config bits
|
||||||
uint16_t config_done; // Configuration bitmask
|
uint16_t config_done; // Configuration bitmask
|
||||||
uint16_t remote_config_rsp_result; // Remote config response result
|
uint16_t remote_config_rsp_result; // Remote config response result
|
||||||
|
|
||||||
tL2CAP_CFG_INFO our_cfg; // Our saved configuration options
|
tL2CAP_CFG_INFO our_cfg; // Our saved configuration options
|
||||||
tL2CAP_CFG_INFO peer_cfg; // Peer's saved configuration options
|
tL2CAP_CFG_INFO peer_cfg; // Peer's saved configuration options
|
||||||
|
|
||||||
// Additional control fields
|
// Additional control fields
|
||||||
uint8_t remote_credit_count; // Credits sent to peer
|
uint8_t remote_credit_count; // Credits sent to peer
|
||||||
tL2C_FCRB fcrb; // FCR info
|
tL2C_FCRB fcrb; // FCR info
|
||||||
bool ecoc; // Enhanced Credit-based mode
|
bool ecoc; // Enhanced Credit-based mode
|
||||||
} tL2C_CCB;
|
} 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) {
|
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
||||||
LOGI("l2c_fcr_chk_chan_modes hooked");
|
LOGI("l2c_fcr_chk_chan_modes hooked");
|
||||||
|
|
||||||
auto* ccb = static_cast<tL2C_CCB*>(p_ccb);
|
auto* ccb = static_cast<tL2C_CCB*>(p_ccb);
|
||||||
|
|
||||||
LOGI("Original FCR mode: 0x%02x", ccb->our_cfg.fcr.mode);
|
LOGI("Original FCR mode: 0x%02x", ccb->our_cfg.fcr.mode);
|
||||||
|
|
||||||
ccb->our_cfg.fcr.mode = 0;
|
ccb->our_cfg.fcr.mode = 0;
|
||||||
|
|
||||||
ccb->our_cfg.fcr_present = true;
|
ccb->our_cfg.fcr_present = true;
|
||||||
|
|
||||||
ccb->peer_cfg.fcr.mode = 0;
|
ccb->peer_cfg.fcr.mode = 0;
|
||||||
ccb->peer_cfg.fcr_present = true;
|
ccb->peer_cfg.fcr_present = true;
|
||||||
|
|
||||||
@@ -141,14 +144,38 @@ uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
uintptr_t loadHookOffset([[maybe_unused]] const char* package_name) {
|
uintptr_t loadHookOffset([[maybe_unused]] const char* package_name) {
|
||||||
const char* property_name = "persist.aln.hook_offset";
|
const char* property_name = "persist.librepods.hook_offset";
|
||||||
char value[PROP_VALUE_MAX] = {0};
|
char value[PROP_VALUE_MAX] = {0};
|
||||||
|
|
||||||
int len = __system_property_get(property_name, value);
|
int len = __system_property_get(property_name, value);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
LOGI("Read hook offset from property: %s", value);
|
LOGI("Read hook offset from property: %s", value);
|
||||||
|
|
||||||
uintptr_t offset;
|
uintptr_t offset;
|
||||||
char* endptr = nullptr;
|
char* endptr = nullptr;
|
||||||
|
|
||||||
@@ -172,6 +199,96 @@ uintptr_t loadHookOffset([[maybe_unused]] const char* package_name) {
|
|||||||
return 0x00a55e30;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
uintptr_t loadL2cCsmConfigOffset() {
|
||||||
|
const char* property_name = "persist.librepods.csm_config_offset";
|
||||||
|
char value[PROP_VALUE_MAX] = {0};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 0 if not found - we'll skip this hook
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uintptr_t getModuleBase(const char *module_name) {
|
uintptr_t getModuleBase(const char *module_name) {
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
char line[1024];
|
char line[1024];
|
||||||
@@ -211,20 +328,84 @@ bool findAndHookFunction([[maybe_unused]] const char *library_path) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uintptr_t offset = loadHookOffset(nullptr);
|
// 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();
|
||||||
|
|
||||||
void* target = reinterpret_cast<void*>(base_addr + offset);
|
bool success = false;
|
||||||
LOGI("Using offset: 0x%x, base: %p, target: %p", offset, (void*)base_addr, target);
|
|
||||||
|
// 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);
|
int result = hook_func(target, (void*)fake_l2c_fcr_chk_chan_modes, (void**)&original_l2c_fcr_chk_chan_modes);
|
||||||
|
if (result != 0) {
|
||||||
if (result == 0) {
|
LOGE("Failed to hook l2c_fcr_chk_chan_modes, error: %d", result);
|
||||||
LOGI("Successfully hooked l2c_fcr_chk_chan_modes");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
LOGE("Failed to hook function, error: %d", result);
|
|
||||||
return false;
|
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) {
|
void on_library_loaded(const char *name, [[maybe_unused]] void *handle) {
|
||||||
|
|||||||
@@ -17,5 +17,12 @@ typedef struct {
|
|||||||
|
|
||||||
[[maybe_unused]] 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 loadHookOffset(const char* package_name);
|
||||||
uintptr_t getModuleBase(const char *module_name);
|
uintptr_t getModuleBase(const char *module_name);
|
||||||
|
uintptr_t loadL2cuProcessCfgReqOffset();
|
||||||
|
uintptr_t loadL2cCsmConfigOffset();
|
||||||
|
uintptr_t loadL2cuSendPeerInfoReqOffset();
|
||||||
|
bool findAndHookFunction(const char *library_path);
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
/*
|
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
|
||||||
*
|
|
||||||
* Copyright (C) 2024 Kavish Devar
|
|
||||||
*
|
|
||||||
* 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.aln.services
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
|
||||||
import android.service.quicksettings.Tile
|
|
||||||
import android.service.quicksettings.TileService
|
|
||||||
import android.util.Log
|
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
|
||||||
import me.kavishdevar.aln.utils.NoiseControlMode
|
|
||||||
|
|
||||||
class AirPodsQSService: TileService() {
|
|
||||||
private val ancModes = listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
|
|
||||||
private var currentModeIndex = 2
|
|
||||||
private lateinit var ancStatusReceiver: BroadcastReceiver
|
|
||||||
private lateinit var availabilityReceiver: BroadcastReceiver
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi", "UnspecifiedRegisterReceiverFlag")
|
|
||||||
override fun onStartListening() {
|
|
||||||
super.onStartListening()
|
|
||||||
currentModeIndex = (ServiceManager.getService()?.getANC()?.minus(1)) ?: -1
|
|
||||||
if (currentModeIndex == -1) {
|
|
||||||
currentModeIndex = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ServiceManager.getService() == null) {
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
ancStatusReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val ancStatus = intent.getIntExtra("data", 4)
|
|
||||||
currentModeIndex = if (ancStatus == 2) 0 else if (ancStatus == 3) 1 else if (ancStatus == 4) 2 else 2
|
|
||||||
updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
availabilityReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action == AirPodsNotifications.Companion.AIRPODS_CONNECTED) {
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
else if (intent.action == AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) {
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
registerReceiver(
|
|
||||||
ancStatusReceiver,
|
|
||||||
IntentFilter(AirPodsNotifications.Companion.ANC_DATA), RECEIVER_EXPORTED
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
registerReceiver(
|
|
||||||
ancStatusReceiver,
|
|
||||||
IntentFilter(AirPodsNotifications.Companion.ANC_DATA)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
qsTile.state = if (ServiceManager.getService()?.isConnectedLocally == true) Tile.STATE_ACTIVE else Tile.STATE_UNAVAILABLE
|
|
||||||
val ancIndex = ServiceManager.getService()?.getANC()
|
|
||||||
currentModeIndex = if (ancIndex != null) { if (ancIndex == 2) 0 else if (ancIndex == 3) 1 else if (ancIndex == 4) 2 else 2 } else 0
|
|
||||||
updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopListening() {
|
|
||||||
super.onStopListening()
|
|
||||||
try {
|
|
||||||
unregisterReceiver(ancStatusReceiver)
|
|
||||||
}
|
|
||||||
catch (
|
|
||||||
_: IllegalArgumentException
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Log.e("QuickSettingTileService", "Receiver not registered")
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
unregisterReceiver(availabilityReceiver)
|
|
||||||
}
|
|
||||||
catch (
|
|
||||||
_: IllegalArgumentException
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Log.e("QuickSettingTileService", "Receiver not registered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick() {
|
|
||||||
super.onClick()
|
|
||||||
Log.d("QuickSettingTileService", "ANC tile clicked")
|
|
||||||
currentModeIndex = (currentModeIndex + 1) % ancModes.size
|
|
||||||
Log.d("QuickSettingTileService", "New mode index: $currentModeIndex, would be set to ${currentModeIndex + 1}")
|
|
||||||
switchAncMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTile() {
|
|
||||||
val currentMode = ancModes[currentModeIndex % ancModes.size]
|
|
||||||
qsTile.label = currentMode.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun switchAncMode() {
|
|
||||||
val airPodsService = ServiceManager.getService()
|
|
||||||
Log.d("QuickSettingTileService", "Setting ANC mode to ${currentModeIndex + 2}")
|
|
||||||
airPodsService?.setANCMode(currentModeIndex + 2)
|
|
||||||
Log.d("QuickSettingTileService", "ANC mode set to ${currentModeIndex + 2}")
|
|
||||||
updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package me.kavishdevar.aln.utils
|
|
||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.util.Log
|
|
||||||
import io.github.libxposed.api.XposedInterface
|
|
||||||
import io.github.libxposed.api.XposedModule
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
|
|
||||||
|
|
||||||
private const val TAG = "AirPodsHook"
|
|
||||||
private lateinit var module: KotlinModule
|
|
||||||
|
|
||||||
class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) {
|
|
||||||
init {
|
|
||||||
Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}")
|
|
||||||
module = this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPackageLoaded(param: XposedModuleInterface.PackageLoadedParam) {
|
|
||||||
super.onPackageLoaded(param)
|
|
||||||
Log.i(TAG, "onPackageLoaded :: ${param.packageName}")
|
|
||||||
|
|
||||||
if (param.packageName == "com.android.bluetooth") {
|
|
||||||
Log.i(TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (param.isFirstPackage) {
|
|
||||||
Log.i(TAG, "Loading native library for Bluetooth hook")
|
|
||||||
System.loadLibrary("l2c_fcr_hook")
|
|
||||||
Log.i(TAG, "Native library loaded successfully")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to load native library: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getApplicationInfo(): ApplicationInfo {
|
|
||||||
return super.applicationInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -47,7 +47,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.aln.ui.theme.ALNTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ class CustomDevice : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
ALNTheme {
|
LibrePodsTheme {
|
||||||
val connect = remember { mutableStateOf(false) }
|
val connect = remember { mutableStateOf(false) }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@@ -100,18 +100,18 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
import me.kavishdevar.aln.screens.AirPodsSettingsScreen
|
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
||||||
import me.kavishdevar.aln.screens.AppSettingsScreen
|
import me.kavishdevar.librepods.screens.AppSettingsScreen
|
||||||
import me.kavishdevar.aln.screens.DebugScreen
|
import me.kavishdevar.librepods.screens.DebugScreen
|
||||||
import me.kavishdevar.aln.screens.HeadTrackingScreen
|
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
||||||
import me.kavishdevar.aln.screens.LongPress
|
import me.kavishdevar.librepods.screens.LongPress
|
||||||
import me.kavishdevar.aln.screens.Onboarding
|
import me.kavishdevar.librepods.screens.Onboarding
|
||||||
import me.kavishdevar.aln.screens.RenameScreen
|
import me.kavishdevar.librepods.screens.RenameScreen
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.aln.ui.theme.ALNTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
import me.kavishdevar.aln.utils.CrossDevice
|
import me.kavishdevar.librepods.utils.CrossDevice
|
||||||
import me.kavishdevar.aln.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
|
|
||||||
lateinit var serviceConnection: ServiceConnection
|
lateinit var serviceConnection: ServiceConnection
|
||||||
lateinit var connectionStatusReceiver: BroadcastReceiver
|
lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||||
@@ -129,7 +129,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
ALNTheme {
|
LibrePodsTheme {
|
||||||
getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor",
|
getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor",
|
||||||
MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply()
|
MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply()
|
||||||
Main()
|
Main()
|
||||||
@@ -180,6 +180,7 @@ fun Main() {
|
|||||||
val hookAvailable = RadareOffsetFinder(LocalContext.current).isHookOffsetAvailable()
|
val hookAvailable = RadareOffsetFinder(LocalContext.current).isHookOffsetAvailable()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
||||||
|
val overlaySkipped = remember { mutableStateOf(context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("overlay_permission_skipped", false)) }
|
||||||
|
|
||||||
val permissionState = rememberMultiplePermissionsState(
|
val permissionState = rememberMultiplePermissionsState(
|
||||||
permissions = listOf(
|
permissions = listOf(
|
||||||
@@ -197,7 +198,7 @@ fun Main() {
|
|||||||
canDrawOverlays = Settings.canDrawOverlays(context)
|
canDrawOverlays = Settings.canDrawOverlays(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permissionState.allPermissionsGranted && canDrawOverlays) {
|
if (permissionState.allPermissionsGranted && (canDrawOverlays || overlaySkipped.value)) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
context.startService(Intent(context, AirPodsService::class.java))
|
context.startService(Intent(context, AirPodsService::class.java))
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ fun Main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionsScreen(
|
fun PermissionsScreen(
|
||||||
permissionState: MultiplePermissionsState,
|
permissionState: MultiplePermissionsState,
|
||||||
@@ -325,6 +326,8 @@ fun PermissionsScreen(
|
|||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
val basicPermissionsGranted = permissionState.permissions.all { it.status.isGranted }
|
||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
|
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
|
||||||
val pulseScale by infiniteTransition.animateFloat(
|
val pulseScale by infiniteTransition.animateFloat(
|
||||||
initialValue = 1f,
|
initialValue = 1f,
|
||||||
@@ -514,6 +517,39 @@ fun PermissionsScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!canDrawOverlays && basicPermissionsGranted) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit()
|
||||||
|
editor.putBoolean("overlay_permission_skipped", true)
|
||||||
|
editor.apply()
|
||||||
|
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(55.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color(0xFF757575)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Continue without overlay",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = Color.White
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,621 @@
|
|||||||
|
package me.kavishdevar.librepods
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.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.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
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.VerticalVolumeSlider
|
||||||
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
|
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
data class DismissAnimationValues(
|
||||||
|
val offsetY: Dp = 0.dp,
|
||||||
|
val scale: Float = 1f,
|
||||||
|
val alpha: Float = 1f
|
||||||
|
)
|
||||||
|
|
||||||
|
class QuickSettingsDialogActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
private var airPodsService: AirPodsService? = null
|
||||||
|
private var isBound = false
|
||||||
|
|
||||||
|
private var isNoiseControlExpandedState by mutableStateOf(false)
|
||||||
|
|
||||||
|
private val connection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
|
val binder = service as AirPodsService.LocalBinder
|
||||||
|
airPodsService = binder.getService()
|
||||||
|
isBound = true
|
||||||
|
Log.d("QSActivity", "Service bound")
|
||||||
|
setContent {
|
||||||
|
LibrePodsTheme {
|
||||||
|
DraggableDismissBox(
|
||||||
|
onDismiss = { finish() },
|
||||||
|
onlyCollapseWhenClicked = {
|
||||||
|
if (isNoiseControlExpandedState) {
|
||||||
|
isNoiseControlExpandedState = false
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (isBound && airPodsService != null) {
|
||||||
|
NewControlCenterDialogContent(
|
||||||
|
service = airPodsService,
|
||||||
|
isNoiseControlExpanded = isNoiseControlExpandedState,
|
||||||
|
onNoiseControlExpandedChange = { isNoiseControlExpandedState = it }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||||
|
isBound = false
|
||||||
|
airPodsService = null
|
||||||
|
Log.d("QSActivity", "Service unbound")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
|
||||||
|
window.setGravity(Gravity.BOTTOM)
|
||||||
|
|
||||||
|
Intent(this, AirPodsService::class.java).also { intent ->
|
||||||
|
bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
LibrePodsTheme {
|
||||||
|
DraggableDismissBox(
|
||||||
|
onDismiss = { finish() },
|
||||||
|
onlyCollapseWhenClicked = {
|
||||||
|
if (isNoiseControlExpandedState) {
|
||||||
|
isNoiseControlExpandedState = false
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (isBound && airPodsService != null) {
|
||||||
|
NewControlCenterDialogContent(
|
||||||
|
service = airPodsService,
|
||||||
|
isNoiseControlExpanded = isNoiseControlExpandedState,
|
||||||
|
onNoiseControlExpandedChange = { isNoiseControlExpandedState = it }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (isBound) {
|
||||||
|
unbindService(connection)
|
||||||
|
isBound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DraggableDismissBox(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onlyCollapseWhenClicked: () -> Boolean,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
var dragOffset by remember { mutableFloatStateOf(0f) }
|
||||||
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
|
val dismissThreshold = 400f
|
||||||
|
|
||||||
|
val animatedOffset = remember { Animatable(0f) }
|
||||||
|
val animatedScale = remember { Animatable(1f) }
|
||||||
|
val animatedAlpha = remember { Animatable(1f) }
|
||||||
|
|
||||||
|
val backgroundAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (isDragging) {
|
||||||
|
val dragProgress = (abs(dragOffset) / 800f).coerceIn(0f, 0.8f)
|
||||||
|
1f - dragProgress
|
||||||
|
} else 1f,
|
||||||
|
label = "BackgroundFade"
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(isDragging) {
|
||||||
|
if (!isDragging) {
|
||||||
|
if (abs(dragOffset) < dismissThreshold) {
|
||||||
|
val springSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessHigh,
|
||||||
|
visibilityThreshold = 0.1f
|
||||||
|
)
|
||||||
|
launch { animatedOffset.animateTo(0f, springSpec) }
|
||||||
|
launch { animatedScale.animateTo(1f, springSpec) }
|
||||||
|
launch { animatedAlpha.animateTo(1f, tween(100)) }
|
||||||
|
dragOffset = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(dragOffset, isDragging) {
|
||||||
|
if (isDragging) {
|
||||||
|
val dragDirection = if (dragOffset > 0) 1f else -1f
|
||||||
|
val dragProgress = (abs(dragOffset) / 1000f).coerceIn(0f, 0.5f)
|
||||||
|
|
||||||
|
animatedOffset.snapTo(dragOffset)
|
||||||
|
animatedScale.snapTo(1f - dragProgress * 0.3f)
|
||||||
|
animatedAlpha.snapTo(1f - dragProgress * 0.7f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.5f * backgroundAlpha))
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectVerticalDragGestures(
|
||||||
|
onDragStart = { isDragging = true },
|
||||||
|
onDragEnd = {
|
||||||
|
isDragging = false
|
||||||
|
if (abs(dragOffset) > dismissThreshold) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val direction = if (dragOffset > 0) 1f else -1f
|
||||||
|
|
||||||
|
launch {
|
||||||
|
animatedOffset.animateTo(
|
||||||
|
direction * 1500f,
|
||||||
|
tween(350, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
launch { animatedScale.animateTo(0.7f, tween(350)) }
|
||||||
|
launch { animatedAlpha.animateTo(0f, tween(250)) }
|
||||||
|
|
||||||
|
kotlinx.coroutines.delay(350)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragCancel = { isDragging = false },
|
||||||
|
onVerticalDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
dragOffset += dragAmount
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null
|
||||||
|
) {
|
||||||
|
onlyCollapseWhenClicked()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.graphicsLayer(
|
||||||
|
translationY = animatedOffset.value,
|
||||||
|
scaleX = animatedScale.value,
|
||||||
|
scaleY = animatedScale.value,
|
||||||
|
alpha = animatedAlpha.value
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewControlCenterDialogContent(
|
||||||
|
service: AirPodsService?,
|
||||||
|
isNoiseControlExpanded: Boolean,
|
||||||
|
onNoiseControlExpandedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
val textColor = Color.White
|
||||||
|
|
||||||
|
var currentAncMode by remember { mutableStateOf(NoiseControlMode.TRANSPARENCY) }
|
||||||
|
var isConvAwarenessEnabled by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val isOffModeEnabled = remember { sharedPreferences.getBoolean("off_listening_mode", true) }
|
||||||
|
val availableModes = remember(isOffModeEnabled) {
|
||||||
|
mutableListOf(
|
||||||
|
NoiseControlMode.TRANSPARENCY,
|
||||||
|
NoiseControlMode.ADAPTIVE,
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION
|
||||||
|
).apply {
|
||||||
|
if (isOffModeEnabled) {
|
||||||
|
add(0, NoiseControlMode.OFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
|
||||||
|
var currentVolumeInt by remember { mutableIntStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) }
|
||||||
|
val animatedVolumeFraction by animateFloatAsState(
|
||||||
|
targetValue = currentVolumeInt.toFloat() / maxVolume.toFloat(),
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessMediumLow
|
||||||
|
),
|
||||||
|
label = "VolumeAnimation"
|
||||||
|
)
|
||||||
|
var liveDragFraction by remember { mutableFloatStateOf(animatedVolumeFraction) }
|
||||||
|
var isDraggingVolume by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(animatedVolumeFraction, isDraggingVolume) {
|
||||||
|
if (!isDraggingVolume) {
|
||||||
|
liveDragFraction = animatedVolumeFraction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(service, availableModes) {
|
||||||
|
val ancReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == AirPodsNotifications.ANC_DATA && service != null) {
|
||||||
|
val newModeOrdinal = intent.getIntExtra("data", NoiseControlMode.TRANSPARENCY.ordinal + 1) - 1
|
||||||
|
val newMode = NoiseControlMode.entries.getOrElse(newModeOrdinal) { NoiseControlMode.TRANSPARENCY }
|
||||||
|
if (availableModes.contains(newMode)) {
|
||||||
|
currentAncMode = newMode
|
||||||
|
} else if (newMode == NoiseControlMode.OFF && !isOffModeEnabled) {
|
||||||
|
currentAncMode = NoiseControlMode.TRANSPARENCY
|
||||||
|
}
|
||||||
|
Log.d("QSActivity", "ANC Receiver updated mode to: $currentAncMode (available: ${availableModes.joinToString()})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val filter = IntentFilter(AirPodsNotifications.ANC_DATA)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(ancReceiver, filter, Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
context.registerReceiver(ancReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
service?.let {
|
||||||
|
val initialModeOrdinal = it.getANC().minus(1) ?: NoiseControlMode.TRANSPARENCY.ordinal
|
||||||
|
var initialMode = NoiseControlMode.entries.getOrElse(initialModeOrdinal) { NoiseControlMode.TRANSPARENCY }
|
||||||
|
if (!availableModes.contains(initialMode)) {
|
||||||
|
initialMode = NoiseControlMode.TRANSPARENCY
|
||||||
|
}
|
||||||
|
currentAncMode = initialMode
|
||||||
|
isConvAwarenessEnabled = sharedPreferences.getBoolean("conversational_awareness", true)
|
||||||
|
Log.d("QSActivity", "Initial ANC: $currentAncMode, ConvAware: $isConvAwarenessEnabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
context.unregisterReceiver(ancReceiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val volumeReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
|
||||||
|
val newVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
if (newVolume != currentVolumeInt) {
|
||||||
|
currentVolumeInt = newVolume
|
||||||
|
Log.d("QSActivity", "Volume Receiver updated volume to: $currentVolumeInt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val filter = IntentFilter("android.media.VOLUME_CHANGED_ACTION")
|
||||||
|
context.registerReceiver(volumeReceiver, filter)
|
||||||
|
onDispose {
|
||||||
|
context.unregisterReceiver(volumeReceiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceName = remember { sharedPreferences.getString("name", "AirPods") ?: "AirPods" }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
awaitPointerEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
if (service != null) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(2f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.airpods),
|
||||||
|
contentDescription = "Device Icon",
|
||||||
|
tint = textColor.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = deviceName,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
VerticalVolumeSlider(
|
||||||
|
displayFraction = animatedVolumeFraction,
|
||||||
|
maxVolume = maxVolume,
|
||||||
|
onVolumeChange = { newVolume ->
|
||||||
|
currentVolumeInt = newVolume
|
||||||
|
try {
|
||||||
|
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0)
|
||||||
|
} catch (e: Exception) { Log.e("QSActivity", "Failed to set volume", e) }
|
||||||
|
},
|
||||||
|
initialFraction = animatedVolumeFraction,
|
||||||
|
onDragStateChange = { dragging -> isDraggingVolume = dragging },
|
||||||
|
baseSliderHeight = 400.dp,
|
||||||
|
baseSliderWidth = 145.dp,
|
||||||
|
baseCornerRadius = 48.dp,
|
||||||
|
maxStretchFactor = 1.15f,
|
||||||
|
minCompressionFactor = 0.875f,
|
||||||
|
stretchSensitivity = 0.3f,
|
||||||
|
compressionSensitivity = 0.3f,
|
||||||
|
cornerRadiusChangeFactor = -0.5f,
|
||||||
|
directionalStretchRatio = 0.75f,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(145.dp)
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 72.dp)
|
||||||
|
.animateContentSize(
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Crossfade(
|
||||||
|
targetState = isNoiseControlExpanded,
|
||||||
|
animationSpec = tween(durationMillis = 300),
|
||||||
|
label = "NoiseControlCrossfade"
|
||||||
|
) { expanded ->
|
||||||
|
if (expanded) {
|
||||||
|
ControlCenterNoiseControlSegmentedButton(
|
||||||
|
availableModes = availableModes,
|
||||||
|
selectedMode = currentAncMode,
|
||||||
|
onModeSelected = { newMode ->
|
||||||
|
service.setANCMode(newMode.ordinal + 1)
|
||||||
|
currentAncMode = newMode
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(0.8f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.85f),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
val noiseControlButtonBrush = if (currentAncMode == NoiseControlMode.ADAPTIVE) {
|
||||||
|
AdaptiveRainbowBrush
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(IconAreaSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
brush = noiseControlButtonBrush ?:
|
||||||
|
Brush.linearGradient(colors = listOf(Color(0xFF0A84FF), Color(0xFF0A84FF)))
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
onClick = { onNoiseControlExpandedChange(true) },
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = getModeIconRes(currentAncMode)),
|
||||||
|
contentDescription = getModeLabel(currentAncMode),
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = getModeLabel(currentAncMode),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(IconAreaSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
if (isConvAwarenessEnabled) Color(0xFF0A84FF) else Color(0x593C3C3E),
|
||||||
|
if (isConvAwarenessEnabled) Color(0xFF0A84FF) else Color(0x593C3C3E)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
val newState = !isConvAwarenessEnabled
|
||||||
|
service.setCAEnabled(newState)
|
||||||
|
isConvAwarenessEnabled = newState
|
||||||
|
},
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.airpods),
|
||||||
|
contentDescription = "Conversational Awareness",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Conversational\nAwareness",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
lineHeight = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Loading...", color = textColor)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModeIconRes(mode: NoiseControlMode): Int {
|
||||||
|
return when (mode) {
|
||||||
|
NoiseControlMode.OFF -> R.drawable.noise_cancellation
|
||||||
|
NoiseControlMode.TRANSPARENCY -> R.drawable.transparency
|
||||||
|
NoiseControlMode.ADAPTIVE -> R.drawable.adaptive
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION -> R.drawable.noise_cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModeLabel(mode: NoiseControlMode): String {
|
||||||
|
return when (mode) {
|
||||||
|
NoiseControlMode.OFF -> "Off"
|
||||||
|
NoiseControlMode.TRANSPARENCY -> "Transparency"
|
||||||
|
NoiseControlMode.ADAPTIVE -> "Adaptive"
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -45,8 +45,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -50,7 +50,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -37,8 +37,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -48,7 +48,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -44,12 +44,12 @@ import androidx.compose.ui.res.imageResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
import me.kavishdevar.aln.utils.Battery
|
import me.kavishdevar.librepods.utils.Battery
|
||||||
import me.kavishdevar.aln.utils.BatteryComponent
|
import me.kavishdevar.librepods.utils.BatteryComponent
|
||||||
import me.kavishdevar.aln.utils.BatteryStatus
|
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Kavish Devar
|
||||||
|
*
|
||||||
|
* 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.composables
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
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.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
private val SelectedColorBlue = Color(0xFF0A84FF)
|
||||||
|
private val UnselectedColor = Color(0x593C3C3E)
|
||||||
|
private val TextColor = Color.White
|
||||||
|
private val IconTint = Color.White
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ControlCenterButton(
|
||||||
|
label: String,
|
||||||
|
icon: Painter,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
iconAreaSize: Dp,
|
||||||
|
isSelected: Boolean,
|
||||||
|
backgroundBrush: Brush? = null
|
||||||
|
) {
|
||||||
|
val targetBackgroundColor = if (isSelected) SelectedColorBlue else UnselectedColor
|
||||||
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = targetBackgroundColor,
|
||||||
|
label = "ButtonBackground"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(iconAreaSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(backgroundBrush ?: Brush.linearGradient(colors=listOf(backgroundColor, backgroundColor)))
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = IconTint,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = TextColor,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Kavish Devar
|
||||||
|
*
|
||||||
|
* 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.composables
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||||
|
|
||||||
|
private val ContainerColor = Color(0x593C3C3E)
|
||||||
|
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
|
||||||
|
private val SelectedIndicatorColorBlue = Color(0xFF0A84FF)
|
||||||
|
private val TextColor = Color.White
|
||||||
|
private val IconTintUnselected = Color.White
|
||||||
|
private val IconTintSelected = Color.White
|
||||||
|
|
||||||
|
internal val AdaptiveRainbowBrush = Brush.sweepGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFFB03A2F), Color(0xFFB07A2F), Color(0xFFB0A22F), Color(0xFF6AB02F),
|
||||||
|
Color(0xFF2FAAB0), Color(0xFF2F5EB0), Color(0xFF7D2FB0), Color(0xFFB02F7D),
|
||||||
|
Color(0xFFB03A2F)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val IconAreaSize = 72.dp
|
||||||
|
private val IconSize = 42.dp
|
||||||
|
private val IconRowHeight = IconAreaSize + 12.dp
|
||||||
|
private val TextRowHeight = 24.dp
|
||||||
|
private val TextSize = 12.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ControlCenterNoiseControlSegmentedButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
availableModes: List<NoiseControlMode>,
|
||||||
|
selectedMode: NoiseControlMode,
|
||||||
|
onModeSelected: (NoiseControlMode) -> Unit
|
||||||
|
) {
|
||||||
|
val selectedIndex = availableModes.indexOf(selectedMode).coerceAtLeast(0)
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var iconRowWidthPx by remember { mutableStateOf(0f) }
|
||||||
|
val itemCount = availableModes.size
|
||||||
|
|
||||||
|
val itemSlotWidthPx = remember(iconRowWidthPx, itemCount) {
|
||||||
|
if (itemCount > 0 && iconRowWidthPx > 0) {
|
||||||
|
iconRowWidthPx / itemCount
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val itemSlotWidthDp = remember(itemSlotWidthPx) { with(density) { itemSlotWidthPx.toDp() } }
|
||||||
|
val iconAreaSizePx = remember { with(density) { IconAreaSize.toPx() } }
|
||||||
|
|
||||||
|
val targetIndicatorStartPx = remember(selectedIndex, itemSlotWidthPx, iconAreaSizePx) {
|
||||||
|
if (itemSlotWidthPx > 0) {
|
||||||
|
val slotCenterPx = (selectedIndex + 0.5f) * itemSlotWidthPx
|
||||||
|
slotCenterPx - (iconAreaSizePx / 2f)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val indicatorOffset: Dp by animateDpAsState(
|
||||||
|
targetValue = with(density) { targetIndicatorStartPx.toDp() },
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
),
|
||||||
|
label = "IndicatorOffset"
|
||||||
|
)
|
||||||
|
|
||||||
|
val indicatorBackground = remember(selectedMode) {
|
||||||
|
when (selectedMode) {
|
||||||
|
NoiseControlMode.ADAPTIVE -> AdaptiveRainbowBrush
|
||||||
|
NoiseControlMode.OFF -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorGray, SelectedIndicatorColorGray))
|
||||||
|
NoiseControlMode.TRANSPARENCY,
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorBlue, SelectedIndicatorColorBlue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IconRowHeight)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(ContainerColor)
|
||||||
|
.onSizeChanged { iconRowWidthPx = it.width.toFloat() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.offset(x = indicatorOffset)
|
||||||
|
.size(IconAreaSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(indicatorBackground)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceAround
|
||||||
|
) {
|
||||||
|
availableModes.forEach { mode ->
|
||||||
|
val isSelected = selectedMode == mode
|
||||||
|
NoiseControlIconItem(
|
||||||
|
modifier = Modifier.size(IconAreaSize),
|
||||||
|
mode = mode,
|
||||||
|
isSelected = isSelected,
|
||||||
|
onClick = { onModeSelected(mode) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(TextRowHeight),
|
||||||
|
horizontalArrangement = Arrangement.SpaceAround,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
availableModes.forEach { mode ->
|
||||||
|
val isSelected = selectedMode == mode
|
||||||
|
Text(
|
||||||
|
text = getModeLabel(mode),
|
||||||
|
color = TextColor,
|
||||||
|
fontSize = TextSize,
|
||||||
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.width(itemSlotWidthDp.coerceAtLeast(1.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NoiseControlIconItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
mode: NoiseControlMode,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val iconRes = remember(mode) { getModeIconRes(mode) }
|
||||||
|
|
||||||
|
val tint = IconTintUnselected
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = iconRes),
|
||||||
|
contentDescription = getModeLabel(mode),
|
||||||
|
tint = if (isSelected && mode == NoiseControlMode.ADAPTIVE) IconTintSelected else tint,
|
||||||
|
modifier = Modifier.size(IconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getModeIconRes(mode: NoiseControlMode): Int {
|
||||||
|
return when (mode) {
|
||||||
|
NoiseControlMode.OFF -> R.drawable.noise_cancellation
|
||||||
|
NoiseControlMode.TRANSPARENCY -> R.drawable.transparency
|
||||||
|
NoiseControlMode.ADAPTIVE -> R.drawable.adaptive
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION -> R.drawable.noise_cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModeLabel(mode: NoiseControlMode): String {
|
||||||
|
return when (mode) {
|
||||||
|
NoiseControlMode.OFF -> "Off"
|
||||||
|
NoiseControlMode.TRANSPARENCY -> "Transparency"
|
||||||
|
NoiseControlMode.ADAPTIVE -> "Adaptive"
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancellation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -55,7 +55,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
class DropdownItem(val name: String, val onSelect: () -> Unit) {
|
class DropdownItem(val name: String, val onSelect: () -> Unit) {
|
||||||
fun select() {
|
fun select() {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false) {
|
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -35,7 +35,7 @@ import androidx.compose.ui.graphics.ImageBitmap
|
|||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlButton(
|
fun NoiseControlButton(
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@@ -72,15 +72,18 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
import me.kavishdevar.aln.utils.NoiseControlMode
|
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSettings(service: AirPodsService) {
|
fun NoiseControlSettings(
|
||||||
|
service: AirPodsService,
|
||||||
|
onModeSelectedCallback: () -> Unit = {} // Callback parameter remains, but won't finish activity
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val offListeningMode = remember { mutableStateOf(sharedPreferences.getBoolean("off_listening_mode", true)) }
|
val offListeningMode = remember { mutableStateOf(sharedPreferences.getBoolean("off_listening_mode", true)) }
|
||||||
@@ -113,13 +116,27 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
val d3a = remember { mutableFloatStateOf(0f) }
|
val d3a = remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) {
|
fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) {
|
||||||
if (!received && !offListeningMode.value && mode == NoiseControlMode.OFF) {
|
val previousMode = noiseControlMode.value // Store previous mode
|
||||||
noiseControlMode.value = NoiseControlMode.ADAPTIVE
|
|
||||||
|
// Ensure the mode is valid if 'Off' is disabled
|
||||||
|
val targetMode = if (!offListeningMode.value && mode == NoiseControlMode.OFF) {
|
||||||
|
// If trying to select OFF but it's disabled, default to Transparency or Adaptive
|
||||||
|
NoiseControlMode.TRANSPARENCY // Or ADAPTIVE, based on preference
|
||||||
} else {
|
} else {
|
||||||
noiseControlMode.value = mode
|
mode
|
||||||
}
|
}
|
||||||
if (!received) service.setANCMode(mode.ordinal + 1)
|
|
||||||
when (noiseControlMode.value) {
|
noiseControlMode.value = targetMode // Update internal state immediately
|
||||||
|
|
||||||
|
// Only call service if the mode was manually selected (!received)
|
||||||
|
// and the target mode is actually different from the previous mode
|
||||||
|
if (!received && targetMode != previousMode) {
|
||||||
|
service.setANCMode(targetMode.ordinal + 1)
|
||||||
|
// onModeSelectedCallback() // REMOVE this call to keep dialog open
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update divider alphas based on the *new* mode
|
||||||
|
when (noiseControlMode.value) { // Use the updated noiseControlMode.value
|
||||||
NoiseControlMode.NOISE_CANCELLATION -> {
|
NoiseControlMode.NOISE_CANCELLATION -> {
|
||||||
d1a.floatValue = 1f
|
d1a.floatValue = 1f
|
||||||
d2a.floatValue = 1f
|
d2a.floatValue = 1f
|
||||||
@@ -312,9 +329,10 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
1 -> if (offListeningMode.value) NoiseControlMode.TRANSPARENCY else NoiseControlMode.ADAPTIVE
|
1 -> if (offListeningMode.value) NoiseControlMode.TRANSPARENCY else NoiseControlMode.ADAPTIVE
|
||||||
2 -> if (offListeningMode.value) NoiseControlMode.ADAPTIVE else NoiseControlMode.NOISE_CANCELLATION
|
2 -> if (offListeningMode.value) NoiseControlMode.ADAPTIVE else NoiseControlMode.NOISE_CANCELLATION
|
||||||
3 -> NoiseControlMode.NOISE_CANCELLATION
|
3 -> NoiseControlMode.NOISE_CANCELLATION
|
||||||
else -> null
|
else -> noiseControlMode.value // Keep current if index is invalid
|
||||||
}
|
}
|
||||||
newMode?.let { onModeSelected(it) }
|
// Call onModeSelected which now handles service call but not callback
|
||||||
|
onModeSelected(newMode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -429,5 +447,5 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
@Preview()
|
@Preview()
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSettingsPreview() {
|
fun NoiseControlSettingsPreview() {
|
||||||
NoiseControlSettings(AirPodsService())
|
NoiseControlSettings(AirPodsService()) {}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -56,7 +56,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PressAndHoldSettings(navController: NavController) {
|
fun PressAndHoldSettings(navController: NavController) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -50,8 +50,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -35,8 +35,8 @@ import androidx.compose.ui.text.font.Font
|
|||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Kavish Devar
|
||||||
|
*
|
||||||
|
* 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.composables
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalVolumeSlider(
|
||||||
|
displayFraction: Float,
|
||||||
|
maxVolume: Int,
|
||||||
|
onVolumeChange: (Int) -> Unit,
|
||||||
|
initialFraction: Float,
|
||||||
|
onDragStateChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
baseSliderHeight: Dp = 400.dp,
|
||||||
|
baseSliderWidth: Dp = 145.dp,
|
||||||
|
baseCornerRadius: Dp = 45.dp,
|
||||||
|
maxStretchFactor: Float = 1.15f,
|
||||||
|
minCompressionFactor: Float = 0.875f,
|
||||||
|
stretchSensitivity: Float = 1.0f,
|
||||||
|
compressionSensitivity: Float = 1.0f,
|
||||||
|
cornerRadiusChangeFactor: Float = 0.2f,
|
||||||
|
directionalStretchRatio: Float = 0.75f
|
||||||
|
) {
|
||||||
|
val trackColor = Color(0x593C3C3E)
|
||||||
|
val progressColor = Color.White
|
||||||
|
|
||||||
|
var dragFraction by remember { mutableFloatStateOf(initialFraction) }
|
||||||
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var rawDragPosition by remember { mutableFloatStateOf(initialFraction) }
|
||||||
|
var overscrollAmount by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
val baseHeightPx = with(LocalDensity.current) { baseSliderHeight.toPx() }
|
||||||
|
|
||||||
|
val animatedProgress by animateFloatAsState(
|
||||||
|
targetValue = dragFraction.coerceIn(0f, 1f),
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
),
|
||||||
|
label = "ProgressAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedOverscroll by animateFloatAsState(
|
||||||
|
targetValue = overscrollAmount,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessMediumLow
|
||||||
|
),
|
||||||
|
label = "OverscrollAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
|
val maxOverscrollEffect = (maxStretchFactor - 1f).coerceAtLeast(0f)
|
||||||
|
|
||||||
|
val stretchMultiplier = stretchSensitivity
|
||||||
|
val compressionMultiplier = compressionSensitivity
|
||||||
|
|
||||||
|
val overscrollDirection = sign(animatedOverscroll)
|
||||||
|
|
||||||
|
val totalStretchAmount = (min(maxOverscrollEffect, abs(animatedOverscroll) * stretchMultiplier) * baseSliderHeight.value).dp
|
||||||
|
|
||||||
|
val offsetY = if (abs(animatedOverscroll) > 0.001f) {
|
||||||
|
val asymmetricOffset = totalStretchAmount * (directionalStretchRatio - 0.5f)
|
||||||
|
(-overscrollDirection * asymmetricOffset.value).dp
|
||||||
|
} else {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
val heightStretch = baseSliderHeight + totalStretchAmount
|
||||||
|
|
||||||
|
val widthCompression = baseSliderWidth * max(
|
||||||
|
minCompressionFactor,
|
||||||
|
1f - min(1f - minCompressionFactor, abs(animatedOverscroll) * compressionMultiplier)
|
||||||
|
)
|
||||||
|
|
||||||
|
val dynamicCornerRadius = baseCornerRadius * (1f - min(cornerRadiusChangeFactor, abs(animatedOverscroll) * cornerRadiusChangeFactor * 2f))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(heightStretch)
|
||||||
|
.width(widthCompression)
|
||||||
|
.offset(y = offsetY)
|
||||||
|
.clip(RoundedCornerShape(dynamicCornerRadius))
|
||||||
|
.background(trackColor)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures { offset ->
|
||||||
|
val newFraction = 1f - (offset.y / size.height).coerceIn(0f, 1f)
|
||||||
|
dragFraction = newFraction
|
||||||
|
rawDragPosition = newFraction
|
||||||
|
overscrollAmount = 0f
|
||||||
|
|
||||||
|
val newVolume = (newFraction * maxVolume).roundToInt()
|
||||||
|
onVolumeChange(newVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.draggable(
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
state = rememberDraggableState { delta ->
|
||||||
|
rawDragPosition -= (delta / baseHeightPx)
|
||||||
|
|
||||||
|
dragFraction = rawDragPosition.coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
overscrollAmount = when {
|
||||||
|
rawDragPosition > 1f -> min(1.0f, (rawDragPosition - 1f) * 2.0f)
|
||||||
|
rawDragPosition < 0f -> max(-1.0f, rawDragPosition * 2.0f)
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
val newVolume = (dragFraction * maxVolume).roundToInt()
|
||||||
|
onVolumeChange(newVolume)
|
||||||
|
},
|
||||||
|
onDragStarted = {
|
||||||
|
isDragging = true
|
||||||
|
dragFraction = displayFraction
|
||||||
|
rawDragPosition = displayFraction
|
||||||
|
overscrollAmount = 0f
|
||||||
|
onDragStateChange(true)
|
||||||
|
},
|
||||||
|
onDragStopped = {
|
||||||
|
isDragging = false
|
||||||
|
overscrollAmount = 0f
|
||||||
|
rawDragPosition = dragFraction
|
||||||
|
onDragStateChange(false)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(animatedProgress)
|
||||||
|
.background(progressColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.receivers
|
package me.kavishdevar.librepods.receivers
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
|
||||||
class BootReceiver: BroadcastReceiver() {
|
class BootReceiver: BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
@@ -84,18 +84,18 @@ import dev.chrisbanes.haze.hazeChild
|
|||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.composables.AccessibilitySettings
|
import me.kavishdevar.librepods.composables.AccessibilitySettings
|
||||||
import me.kavishdevar.aln.composables.AudioSettings
|
import me.kavishdevar.librepods.composables.AudioSettings
|
||||||
import me.kavishdevar.aln.composables.BatteryView
|
import me.kavishdevar.librepods.composables.BatteryView
|
||||||
import me.kavishdevar.aln.composables.IndependentToggle
|
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||||
import me.kavishdevar.aln.composables.NameField
|
import me.kavishdevar.librepods.composables.NameField
|
||||||
import me.kavishdevar.aln.composables.NavigationButton
|
import me.kavishdevar.librepods.composables.NavigationButton
|
||||||
import me.kavishdevar.aln.composables.NoiseControlSettings
|
import me.kavishdevar.librepods.composables.NoiseControlSettings
|
||||||
import me.kavishdevar.aln.composables.PressAndHoldSettings
|
import me.kavishdevar.librepods.composables.PressAndHoldSettings
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.aln.ui.theme.ALNTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
@@ -147,11 +147,11 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
val bluetoothReceiver = remember {
|
val bluetoothReceiver = remember {
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (intent?.action == "me.kavishdevar.aln.AIRPODS_CONNECTED_REMOTELY") {
|
if (intent?.action == "me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY") {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
handleRemoteConnection(true)
|
handleRemoteConnection(true)
|
||||||
}
|
}
|
||||||
} else if (intent?.action == "me.kavishdevar.aln.AIRPODS_DISCONNECTED_REMOTELY") {
|
} else if (intent?.action == "me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY") {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
handleRemoteConnection(false)
|
handleRemoteConnection(false)
|
||||||
}
|
}
|
||||||
@@ -168,8 +168,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply {
|
||||||
addAction("me.kavishdevar.aln.AIRPODS_CONNECTED_REMOTELY")
|
addAction("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
||||||
addAction("me.kavishdevar.aln.AIRPODS_DISCONNECTED_REMOTELY")
|
addAction("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
||||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +399,7 @@ fun AirPodsSettingsScreenPreview() {
|
|||||||
Column (
|
Column (
|
||||||
modifier = Modifier.height(2000.dp)
|
modifier = Modifier.height(2000.dp)
|
||||||
) {
|
) {
|
||||||
ALNTheme (
|
LibrePodsTheme (
|
||||||
darkTheme = true
|
darkTheme = true
|
||||||
) {
|
) {
|
||||||
AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true, isRemotelyConnected = false)
|
AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true, isRemotelyConnected = false)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -74,11 +74,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.composables.IndependentToggle
|
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||||
import me.kavishdevar.aln.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.aln.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -419,7 +419,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (RadareOffsetFinder.clearHookOffset()) {
|
if (RadareOffsetFinder.clearHookOffsets()) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"Hook offset has been reset. Redirecting to setup...",
|
"Hook offset has been reset. Redirecting to setup...",
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalHazeMaterialsApi::class)
|
@file:OptIn(ExperimentalHazeMaterialsApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
@@ -99,11 +99,11 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.aln.utils.BatteryStatus
|
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||||
import me.kavishdevar.aln.utils.isHeadTrackingData
|
import me.kavishdevar.librepods.utils.isHeadTrackingData
|
||||||
import me.kavishdevar.aln.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -99,10 +99,10 @@ import androidx.navigation.NavController
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.composables.IndependentToggle
|
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.aln.utils.HeadTracking
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -39,18 +39,24 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -76,8 +82,8 @@ import androidx.navigation.NavController
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -97,6 +103,9 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
var moduleEnabled by remember { mutableStateOf(false) }
|
var moduleEnabled by remember { mutableStateOf(false) }
|
||||||
var bluetoothToggled by remember { mutableStateOf(false) }
|
var bluetoothToggled by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
var showSkipDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun checkRootAccess() {
|
fun checkRootAccess() {
|
||||||
checkingRoot = true
|
checkingRoot = true
|
||||||
rootCheckFailed = false
|
rootCheckFailed = false
|
||||||
@@ -158,7 +167,29 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color.Transparent
|
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)
|
containerColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)
|
||||||
@@ -477,6 +508,51 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +641,7 @@ private fun getStatusDescription(
|
|||||||
return when (state) {
|
return when (state) {
|
||||||
is RadareOffsetFinder.ProgressState.Success -> {
|
is RadareOffsetFinder.ProgressState.Success -> {
|
||||||
when {
|
when {
|
||||||
!moduleEnabled -> "Please enable the ALN Xposed module in your Xposed manager (e.g. LSPosed). If already enabled, disable and re-enable it."
|
!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."
|
!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."
|
else -> "All set! You can now use your AirPods with enhanced functionality."
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -67,8 +67,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
@Composable()
|
@Composable()
|
||||||
fun RightDivider() {
|
fun RightDivider() {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -64,8 +64,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Kavish Devar
|
||||||
|
*
|
||||||
|
* 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.services
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.quicksettings.Tile
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
|
import me.kavishdevar.librepods.utils.NoiseControlMode
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
class AirPodsQSService : TileService() {
|
||||||
|
|
||||||
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
private var currentAncMode: Int = NoiseControlMode.OFF.ordinal + 1
|
||||||
|
private var isAirPodsConnected: Boolean = false
|
||||||
|
|
||||||
|
private val ancStatusReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == AirPodsNotifications.ANC_DATA) {
|
||||||
|
val newMode = intent.getIntExtra("data", NoiseControlMode.OFF.ordinal + 1)
|
||||||
|
Log.d("AirPodsQSService", "Received ANC update: $newMode")
|
||||||
|
currentAncMode = newMode
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val availabilityReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
AirPodsNotifications.AIRPODS_CONNECTED -> {
|
||||||
|
Log.d("AirPodsQSService", "Received AIRPODS_CONNECTED")
|
||||||
|
isAirPodsConnected = true
|
||||||
|
currentAncMode =
|
||||||
|
ServiceManager.getService()?.getANC() ?: (NoiseControlMode.OFF.ordinal + 1)
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
AirPodsNotifications.AIRPODS_DISCONNECTED -> {
|
||||||
|
Log.d("AirPodsQSService", "Received AIRPODS_DISCONNECTED")
|
||||||
|
isAirPodsConnected = false
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
|
if (key == "off_listening_mode") {
|
||||||
|
Log.d("AirPodsQSService", "Preference changed: $key")
|
||||||
|
if (currentAncMode == NoiseControlMode.OFF.ordinal + 1 && !isOffModeEnabled()) {
|
||||||
|
currentAncMode = NoiseControlMode.TRANSPARENCY.ordinal + 1
|
||||||
|
}
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi", "UnspecifiedRegisterReceiverFlag")
|
||||||
|
override fun onStartListening() {
|
||||||
|
super.onStartListening()
|
||||||
|
Log.d("AirPodsQSService", "onStartListening")
|
||||||
|
|
||||||
|
val service = ServiceManager.getService()
|
||||||
|
isAirPodsConnected = service?.isConnectedLocally == true
|
||||||
|
currentAncMode = service?.getANC() ?: (NoiseControlMode.OFF.ordinal + 1)
|
||||||
|
|
||||||
|
if (currentAncMode == NoiseControlMode.OFF.ordinal + 1 && !isOffModeEnabled()) {
|
||||||
|
currentAncMode = NoiseControlMode.TRANSPARENCY.ordinal + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val ancIntentFilter = IntentFilter(AirPodsNotifications.ANC_DATA)
|
||||||
|
val availabilityIntentFilter = IntentFilter().apply {
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
registerReceiver(ancStatusReceiver, ancIntentFilter, RECEIVER_EXPORTED)
|
||||||
|
registerReceiver(availabilityReceiver, availabilityIntentFilter, RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
registerReceiver(ancStatusReceiver, ancIntentFilter)
|
||||||
|
registerReceiver(availabilityReceiver, availabilityIntentFilter)
|
||||||
|
}
|
||||||
|
sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||||
|
Log.d("AirPodsQSService", "Receivers registered")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AirPodsQSService", "Error registering receivers: $e")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopListening() {
|
||||||
|
super.onStopListening()
|
||||||
|
Log.d("AirPodsQSService", "onStopListening")
|
||||||
|
try {
|
||||||
|
unregisterReceiver(ancStatusReceiver)
|
||||||
|
unregisterReceiver(availabilityReceiver)
|
||||||
|
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||||
|
Log.d("AirPodsQSService", "Receivers unregistered")
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Log.e("AirPodsQSService", "Receiver not registered or already unregistered: $e")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AirPodsQSService", "Error unregistering receivers: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
super.onClick()
|
||||||
|
Log.d("AirPodsQSService", "onClick - Current state: $isAirPodsConnected, Current mode: $currentAncMode")
|
||||||
|
if (!isAirPodsConnected) {
|
||||||
|
Log.d("AirPodsQSService", "Tile clicked but AirPods not connected.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val clickBehavior = sharedPreferences.getString("qs_click_behavior", "dialog") ?: "dialog"
|
||||||
|
|
||||||
|
if (clickBehavior == "dialog") {
|
||||||
|
launchDialogActivity()
|
||||||
|
} else {
|
||||||
|
cycleAncMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchDialogActivity() {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
Intent(this, QuickSettingsDialogActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
startActivityAndCollapse(pendingIntent)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val intent = Intent(this, QuickSettingsDialogActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
}
|
||||||
|
startActivityAndCollapse(intent)
|
||||||
|
}
|
||||||
|
Log.d("AirPodsQSService", "Called startActivityAndCollapse for QuickSettingsDialogActivity")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AirPodsQSService", "Error launching QuickSettingsDialogActivity: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cycleAncMode() {
|
||||||
|
val service = ServiceManager.getService()
|
||||||
|
if (service == null) {
|
||||||
|
Log.d("AirPodsQSService", "Tile clicked (cycle mode) but service is null.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val nextMode = getNextAncMode()
|
||||||
|
Log.d("AirPodsQSService", "Cycling ANC mode to: $nextMode")
|
||||||
|
service.setANCMode(nextMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTile() {
|
||||||
|
val tile = qsTile ?: return
|
||||||
|
Log.d("AirPodsQSService", "updateTile - Connected: $isAirPodsConnected, Mode: $currentAncMode")
|
||||||
|
|
||||||
|
val deviceName = sharedPreferences.getString("name", "AirPods") ?: "AirPods"
|
||||||
|
|
||||||
|
if (isAirPodsConnected) {
|
||||||
|
tile.state = Tile.STATE_ACTIVE
|
||||||
|
tile.label = getModeLabel(currentAncMode)
|
||||||
|
tile.subtitle = deviceName
|
||||||
|
tile.icon = Icon.createWithResource(this, getModeIcon(currentAncMode))
|
||||||
|
} else {
|
||||||
|
tile.state = Tile.STATE_UNAVAILABLE
|
||||||
|
tile.label = "AirPods"
|
||||||
|
tile.subtitle = "Disconnected"
|
||||||
|
tile.icon = Icon.createWithResource(this, R.drawable.airpods)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tile.updateTile()
|
||||||
|
Log.d("AirPodsQSService", "Tile updated successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AirPodsQSService", "Error updating tile: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOffModeEnabled(): Boolean {
|
||||||
|
return sharedPreferences.getBoolean("off_listening_mode", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableModes(): List<Int> {
|
||||||
|
val modes = mutableListOf(
|
||||||
|
NoiseControlMode.TRANSPARENCY.ordinal + 1,
|
||||||
|
NoiseControlMode.ADAPTIVE.ordinal + 1,
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION.ordinal + 1
|
||||||
|
)
|
||||||
|
if (isOffModeEnabled()) {
|
||||||
|
modes.add(0, NoiseControlMode.OFF.ordinal + 1)
|
||||||
|
}
|
||||||
|
return modes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNextAncMode(): Int {
|
||||||
|
val availableModes = getAvailableModes()
|
||||||
|
val currentIndex = availableModes.indexOf(currentAncMode)
|
||||||
|
val nextIndex = (currentIndex + 1) % availableModes.size
|
||||||
|
return availableModes[nextIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModeLabel(mode: Int): String {
|
||||||
|
return when (mode) {
|
||||||
|
NoiseControlMode.OFF.ordinal + 1 -> "Off"
|
||||||
|
NoiseControlMode.TRANSPARENCY.ordinal + 1 -> "Transparency"
|
||||||
|
NoiseControlMode.ADAPTIVE.ordinal + 1 -> "Adaptive"
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION.ordinal + 1 -> "Noise Cancellation"
|
||||||
|
else -> "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModeIcon(mode: Int): Int {
|
||||||
|
return when (mode) {
|
||||||
|
NoiseControlMode.OFF.ordinal + 1 -> R.drawable.noise_cancellation
|
||||||
|
NoiseControlMode.TRANSPARENCY.ordinal + 1 -> R.drawable.transparency
|
||||||
|
NoiseControlMode.ADAPTIVE.ordinal + 1 -> R.drawable.adaptive
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION.ordinal + 1 -> R.drawable.noise_cancellation
|
||||||
|
else -> R.drawable.airpods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 Kavish Devar
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.services
|
package me.kavishdevar.librepods.services
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -46,6 +46,7 @@ import android.os.Handler
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
|
import android.provider.Settings
|
||||||
import android.telecom.TelecomManager
|
import android.telecom.TelecomManager
|
||||||
import android.telephony.PhoneStateListener
|
import android.telephony.PhoneStateListener
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
@@ -67,26 +68,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import me.kavishdevar.aln.MainActivity
|
import me.kavishdevar.librepods.MainActivity
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
import me.kavishdevar.aln.utils.Battery
|
import me.kavishdevar.librepods.utils.Battery
|
||||||
import me.kavishdevar.aln.utils.BatteryComponent
|
import me.kavishdevar.librepods.utils.BatteryComponent
|
||||||
import me.kavishdevar.aln.utils.BatteryStatus
|
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||||
import me.kavishdevar.aln.utils.CrossDevice
|
import me.kavishdevar.librepods.utils.CrossDevice
|
||||||
import me.kavishdevar.aln.utils.CrossDevicePackets
|
import me.kavishdevar.librepods.utils.CrossDevicePackets
|
||||||
import me.kavishdevar.aln.utils.Enums
|
import me.kavishdevar.librepods.utils.Enums
|
||||||
import me.kavishdevar.aln.utils.GestureDetector
|
import me.kavishdevar.librepods.utils.GestureDetector
|
||||||
import me.kavishdevar.aln.utils.HeadTracking
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
import me.kavishdevar.aln.utils.IslandType
|
import me.kavishdevar.librepods.utils.IslandType
|
||||||
import me.kavishdevar.aln.utils.IslandWindow
|
import me.kavishdevar.librepods.utils.IslandWindow
|
||||||
import me.kavishdevar.aln.utils.LongPressPackets
|
import me.kavishdevar.librepods.utils.LongPressPackets
|
||||||
import me.kavishdevar.aln.utils.MediaController
|
import me.kavishdevar.librepods.utils.MediaController
|
||||||
import me.kavishdevar.aln.utils.PopupWindow
|
import me.kavishdevar.librepods.utils.PopupWindow
|
||||||
import me.kavishdevar.aln.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import me.kavishdevar.aln.utils.isHeadTrackingData
|
import me.kavishdevar.librepods.utils.isHeadTrackingData
|
||||||
import me.kavishdevar.aln.widgets.BatteryWidget
|
import me.kavishdevar.librepods.widgets.BatteryWidget
|
||||||
import me.kavishdevar.aln.widgets.NoiseControlWidget
|
import me.kavishdevar.librepods.widgets.NoiseControlWidget
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -162,12 +163,11 @@ class AirPodsService : Service() {
|
|||||||
_packetLogsFlow.value = inMemoryLogs.toSet()
|
_packetLogsFlow.value = inMemoryLogs.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to SharedPreferences less frequently - only needed for persistence between sessions
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val logs = sharedPreferencesLogs.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet()
|
val logs = sharedPreferencesLogs.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet()
|
||||||
?: mutableSetOf()
|
?: mutableSetOf()
|
||||||
logs.add(logEntry)
|
logs.add(logEntry)
|
||||||
// Limit SharedPreferences size
|
|
||||||
if (logs.size > maxLogEntries) {
|
if (logs.size > maxLogEntries) {
|
||||||
val toKeep = logs.toList().takeLast(maxLogEntries).toSet()
|
val toKeep = logs.toList().takeLast(maxLogEntries).toSet()
|
||||||
sharedPreferencesLogs.edit { putStringSet(packetLogKey, toKeep) }
|
sharedPreferencesLogs.edit { putStringSet(packetLogKey, toKeep) }
|
||||||
@@ -213,6 +213,10 @@ class AirPodsService : Service() {
|
|||||||
var popupShown = false
|
var popupShown = false
|
||||||
|
|
||||||
fun showPopup(service: Service, name: String) {
|
fun showPopup(service: Service, name: String) {
|
||||||
|
if (!Settings.canDrawOverlays(service)) {
|
||||||
|
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (popupShown) {
|
if (popupShown) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -225,6 +229,10 @@ class AirPodsService : Service() {
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED) {
|
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED) {
|
||||||
Log.d("AirPodsService", "Showing island window")
|
Log.d("AirPodsService", "Showing island window")
|
||||||
|
if (!Settings.canDrawOverlays(service)) {
|
||||||
|
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
|
||||||
|
return
|
||||||
|
}
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
islandWindow = IslandWindow(service.applicationContext)
|
islandWindow = IslandWindow(service.applicationContext)
|
||||||
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this@AirPodsService, type)
|
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this@AirPodsService, type)
|
||||||
@@ -791,7 +799,7 @@ class AirPodsService : Service() {
|
|||||||
}
|
}
|
||||||
val showIslandReceiver = object: BroadcastReceiver() {
|
val showIslandReceiver = object: BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (intent?.action == "me.kavishdevar.aln.cross_device_island") {
|
if (intent?.action == "me.kavishdevar.librepods.cross_device_island") {
|
||||||
showIsland(this@AirPodsService, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!))
|
showIsland(this@AirPodsService, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!))
|
||||||
} else if (intent?.action == AirPodsNotifications.Companion.DISCONNECT_RECEIVERS) {
|
} else if (intent?.action == AirPodsNotifications.Companion.DISCONNECT_RECEIVERS) {
|
||||||
try {
|
try {
|
||||||
@@ -804,7 +812,7 @@ class AirPodsService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val showIslandIntentFilter = IntentFilter().apply {
|
val showIslandIntentFilter = IntentFilter().apply {
|
||||||
addAction("me.kavishdevar.aln.cross_device_island")
|
addAction("me.kavishdevar.librepods.cross_device_island")
|
||||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,7 +1293,6 @@ class AirPodsService : Service() {
|
|||||||
|
|
||||||
fun sendPacket(packet: ByteArray) {
|
fun sendPacket(packet: ByteArray) {
|
||||||
try {
|
try {
|
||||||
// Always log the packet
|
|
||||||
logPacket(packet, "Sent")
|
logPacket(packet, "Sent")
|
||||||
|
|
||||||
if (!isConnectedLocally && CrossDevice.isAvailable) {
|
if (!isConnectedLocally && CrossDevice.isAvailable) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.aln.ui.theme
|
package me.kavishdevar.librepods.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.ui.theme
|
package me.kavishdevar.librepods.ui.theme
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -38,21 +38,11 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
primary = Purple40,
|
primary = Purple40,
|
||||||
secondary = PurpleGrey40,
|
secondary = PurpleGrey40,
|
||||||
tertiary = Pink40
|
tertiary = Pink40
|
||||||
/* Other default colors to override
|
|
||||||
background = Color(0xFFFFFBFE),
|
|
||||||
surface = Color(0xFFFFFBFE),
|
|
||||||
onPrimary = Color.White,
|
|
||||||
onSecondary = Color.White,
|
|
||||||
onTertiary = Color.White,
|
|
||||||
onBackground = Color(0xFF1C1B1F),
|
|
||||||
onSurface = Color(0xFF1C1B1F),
|
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ALNTheme(
|
fun LibrePodsTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.ui.theme
|
package me.kavishdevar.librepods.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -37,7 +37,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ object CrossDevice {
|
|||||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
|
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
|
||||||
this@CrossDevice.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
this@CrossDevice.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
this@CrossDevice.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
this@CrossDevice.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
||||||
startAdvertising()
|
// startAdvertising()
|
||||||
startServer()
|
startServer()
|
||||||
initialized = true
|
initialized = true
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ object CrossDevice {
|
|||||||
)
|
)
|
||||||
if (earDetectionStatus == listOf(false, false) && newEarDetectionStatus.contains(true)) {
|
if (earDetectionStatus == listOf(false, false) && newEarDetectionStatus.contains(true)) {
|
||||||
ServiceManager.getService()?.applicationContext?.sendBroadcast(
|
ServiceManager.getService()?.applicationContext?.sendBroadcast(
|
||||||
Intent("me.kavishdevar.aln.cross_device_island")
|
Intent("me.kavishdevar.librepods.cross_device_island")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
earDetectionStatus = newEarDetectionStatus
|
earDetectionStatus = newEarDetectionStatus
|
||||||
@@ -276,11 +276,11 @@ object CrossDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun notifyAirPodsConnectedRemotely(context: Context) {
|
fun notifyAirPodsConnectedRemotely(context: Context) {
|
||||||
val intent = Intent("me.kavishdevar.aln.AIRPODS_CONNECTED_REMOTELY")
|
val intent = Intent("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
||||||
context.sendBroadcast(intent)
|
context.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
fun notifyAirPodsDisconnectedRemotely(context: Context) {
|
fun notifyAirPodsDisconnectedRemotely(context: Context) {
|
||||||
val intent = Intent("me.kavishdevar.aln.AIRPODS_DISCONNECTED_REMOTELY")
|
val intent = Intent("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
||||||
context.sendBroadcast(intent)
|
context.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -9,8 +9,8 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("PrivatePropertyName")
|
@file:Suppress("PrivatePropertyName")
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
@@ -12,7 +12,7 @@ import android.os.Build
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
@@ -39,8 +39,8 @@ import android.widget.ProgressBar
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.VideoView
|
import android.widget.VideoView
|
||||||
import androidx.core.content.ContextCompat.getString
|
import androidx.core.content.ContextCompat.getString
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
enum class IslandType {
|
enum class IslandType {
|
||||||
CONNECTED,
|
CONNECTED,
|
||||||
@@ -106,7 +106,7 @@ class IslandWindow(context: Context) {
|
|||||||
batteryProgressBar.isIndeterminate = false
|
batteryProgressBar.isIndeterminate = false
|
||||||
|
|
||||||
val videoView = islandView.findViewById<VideoView>(R.id.island_video_view)
|
val videoView = islandView.findViewById<VideoView>(R.id.island_video_view)
|
||||||
val videoUri = Uri.parse("android.resource://me.kavishdevar.aln/${R.raw.island}")
|
val videoUri = Uri.parse("android.resource://me.kavishdevar.librepods/${R.raw.island}")
|
||||||
videoView.setVideoURI(videoUri)
|
videoView.setVideoURI(videoUri)
|
||||||
videoView.setOnPreparedListener { mediaPlayer ->
|
videoView.setOnPreparedListener { mediaPlayer ->
|
||||||
mediaPlayer.isLooping = true
|
mediaPlayer.isLooping = true
|
||||||
@@ -0,0 +1,793 @@
|
|||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import io.github.libxposed.api.XposedInterface.AfterHookCallback
|
||||||
|
import io.github.libxposed.api.XposedModule
|
||||||
|
import io.github.libxposed.api.XposedModuleInterface
|
||||||
|
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
|
||||||
|
import io.github.libxposed.api.annotations.AfterInvocation
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker
|
||||||
|
|
||||||
|
private const val TAG = "AirPodsHook"
|
||||||
|
private lateinit var module: KotlinModule
|
||||||
|
|
||||||
|
class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) {
|
||||||
|
init {
|
||||||
|
Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}")
|
||||||
|
module = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPackageLoaded(param: XposedModuleInterface.PackageLoadedParam) {
|
||||||
|
super.onPackageLoaded(param)
|
||||||
|
Log.i(TAG, "onPackageLoaded :: ${param.packageName}")
|
||||||
|
|
||||||
|
if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
|
||||||
|
Log.i(TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (param.isFirstPackage) {
|
||||||
|
Log.i(TAG, "Loading native library for Bluetooth hook")
|
||||||
|
System.loadLibrary("l2c_fcr_hook")
|
||||||
|
Log.i(TAG, "Native library loaded successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load native library: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.packageName == "com.android.settings") {
|
||||||
|
Log.i(TAG, "Settings app detected, hooking Bluetooth icon handling")
|
||||||
|
try {
|
||||||
|
val headerControllerClass = param.classLoader.loadClass(
|
||||||
|
"com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
|
||||||
|
|
||||||
|
val updateIconMethod = headerControllerClass.getDeclaredMethod(
|
||||||
|
"updateIcon",
|
||||||
|
android.widget.ImageView::class.java,
|
||||||
|
String::class.java)
|
||||||
|
|
||||||
|
hook(updateIconMethod, BluetoothIconHooker::class.java)
|
||||||
|
Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val displayPreferenceMethod = headerControllerClass.getDeclaredMethod(
|
||||||
|
"displayPreference",
|
||||||
|
param.classLoader.loadClass("androidx.preference.PreferenceScreen"))
|
||||||
|
|
||||||
|
hook(displayPreferenceMethod, BluetoothSettingsAirPodsHooker::class.java)
|
||||||
|
Log.i(TAG, "Successfully hooked displayPreference for AirPods button injection")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to hook displayPreference: ${e.message}", e)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
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
|
||||||
|
class BluetoothSettingsAirPodsHooker : XposedInterface.Hooker {
|
||||||
|
companion object {
|
||||||
|
private const val AIRPODS_UUID = "74ec2172-0bad-4d01-8f77-997b2be0722a"
|
||||||
|
private const val LIBREPODS_PREFERENCE_KEY = "librepods_open_preference"
|
||||||
|
private const val ACTION_SET_ANC_MODE = "me.kavishdevar.librepods.SET_ANC_MODE"
|
||||||
|
private const val EXTRA_ANC_MODE = "anc_mode"
|
||||||
|
|
||||||
|
private const val ANC_MODE_OFF = 1
|
||||||
|
private const val ANC_MODE_NOISE_CANCELLATION = 2
|
||||||
|
private const val ANC_MODE_TRANSPARENCY = 3
|
||||||
|
private const val ANC_MODE_ADAPTIVE = 4
|
||||||
|
|
||||||
|
private var currentAncMode = ANC_MODE_NOISE_CANCELLATION
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@AfterInvocation
|
||||||
|
fun afterDisplayPreference(callback: AfterHookCallback) {
|
||||||
|
try {
|
||||||
|
val controller = callback.thisObject!!
|
||||||
|
val preferenceScreen = callback.args[0]!!
|
||||||
|
|
||||||
|
val context = preferenceScreen.javaClass.getMethod("getContext").invoke(preferenceScreen) as Context
|
||||||
|
|
||||||
|
val deviceField = controller.javaClass.getDeclaredField("mCachedDevice")
|
||||||
|
deviceField.isAccessible = true
|
||||||
|
val cachedDevice = deviceField.get(controller) ?: return
|
||||||
|
|
||||||
|
val getDeviceMethod = cachedDevice.javaClass.getMethod("getDevice")
|
||||||
|
val bluetoothDevice = getDeviceMethod.invoke(cachedDevice) ?: return
|
||||||
|
|
||||||
|
val uuidsMethod = bluetoothDevice.javaClass.getMethod("getUuids")
|
||||||
|
val uuids = uuidsMethod.invoke(bluetoothDevice) as? Array<ParcelUuid>
|
||||||
|
|
||||||
|
if (uuids != null) {
|
||||||
|
val isAirPods = uuids.any { it.uuid.toString() == AIRPODS_UUID }
|
||||||
|
|
||||||
|
if (isAirPods) {
|
||||||
|
Log.i(TAG, "AirPods device detected in settings, injecting controls")
|
||||||
|
|
||||||
|
val findPreferenceMethod = preferenceScreen.javaClass.getMethod("findPreference", CharSequence::class.java)
|
||||||
|
val existingPref = findPreferenceMethod.invoke(preferenceScreen, LIBREPODS_PREFERENCE_KEY)
|
||||||
|
|
||||||
|
if (existingPref != null) {
|
||||||
|
Log.i(TAG, "LIBREPODS button already exists, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val preferenceClass = preferenceScreen.javaClass.classLoader.loadClass("androidx.preference.Preference")
|
||||||
|
val preference = preferenceClass.getConstructor(Context::class.java).newInstance(context)
|
||||||
|
|
||||||
|
val setKeyMethod = preferenceClass.getMethod("setKey", String::class.java)
|
||||||
|
setKeyMethod.invoke(preference, LIBREPODS_PREFERENCE_KEY)
|
||||||
|
|
||||||
|
val setTitleMethod = preferenceClass.getMethod("setTitle", CharSequence::class.java)
|
||||||
|
setTitleMethod.invoke(preference, "Open LibrePods")
|
||||||
|
|
||||||
|
val setSummaryMethod = preferenceClass.getMethod("setSummary", CharSequence::class.java)
|
||||||
|
setSummaryMethod.invoke(preference, "Control AirPods features")
|
||||||
|
|
||||||
|
val setIconMethod = preferenceClass.getMethod("setIcon", Int::class.java)
|
||||||
|
setIconMethod.invoke(preference, android.R.drawable.ic_menu_manage)
|
||||||
|
|
||||||
|
val setOrderMethod = preferenceClass.getMethod("setOrder", Int::class.java)
|
||||||
|
setOrderMethod.invoke(preference, 1000)
|
||||||
|
|
||||||
|
val intent = Intent().apply {
|
||||||
|
setClassName("me.kavishdevar.librepods", "me.kavishdevar.librepods.MainActivity")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
val setIntentMethod = preferenceClass.getMethod("setIntent", Intent::class.java)
|
||||||
|
setIntentMethod.invoke(preference, intent)
|
||||||
|
|
||||||
|
val addPreferenceMethod = preferenceScreen.javaClass.getMethod("addPreference", preferenceClass)
|
||||||
|
addPreferenceMethod.invoke(preferenceScreen, preference)
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully added Open LIBREPODS button to AirPods settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error in BluetoothSettingsAirPodsHooker: ${e.message}", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@XposedHooker
|
||||||
|
class BluetoothIconHooker : XposedInterface.Hooker {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@AfterInvocation
|
||||||
|
fun afterUpdateIcon(callback: AfterHookCallback) {
|
||||||
|
Log.i(TAG, "BluetoothIconHooker called with args: ${callback.args.joinToString(", ")}")
|
||||||
|
try {
|
||||||
|
val imageView = callback.args[0] as ImageView
|
||||||
|
val iconUri = callback.args[1] as String
|
||||||
|
|
||||||
|
val uri = android.net.Uri.parse(iconUri)
|
||||||
|
if (uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
|
||||||
|
Log.i(TAG, "Handling AirPods icon URI: $uri")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val context = imageView.context
|
||||||
|
|
||||||
|
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||||
|
try {
|
||||||
|
val packageName = uri.authority
|
||||||
|
val packageContext = context.createPackageContext(
|
||||||
|
packageName,
|
||||||
|
Context.CONTEXT_IGNORE_SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
|
val resPath = uri.pathSegments
|
||||||
|
if (resPath.size >= 2 && resPath[0] == "drawable") {
|
||||||
|
val resourceName = resPath[1]
|
||||||
|
val resourceId = packageContext.resources.getIdentifier(
|
||||||
|
resourceName, "drawable", packageName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (resourceId != 0) {
|
||||||
|
val drawable = packageContext.resources.getDrawable(
|
||||||
|
resourceId, packageContext.theme
|
||||||
|
)
|
||||||
|
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
imageView.alpha = 1.0f
|
||||||
|
|
||||||
|
callback.result = null
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully loaded icon from resource: $resourceName")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Resource not found: $resourceName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error loading resource from URI $uri: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error accessing context: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error in BluetoothIconHooker: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApplicationInfo(): ApplicationInfo {
|
||||||
|
return super.applicationInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ANC_MODE_OFF = 1
|
||||||
|
private const val ANC_MODE_NOISE_CANCELLATION = 2
|
||||||
|
private const val ANC_MODE_TRANSPARENCY = 3
|
||||||
|
private const val ANC_MODE_ADAPTIVE = 4
|
||||||
|
|
||||||
|
private var currentANCMode = ANC_MODE_NOISE_CANCELLATION
|
||||||
|
|
||||||
|
private const val ACTION_SET_ANC_MODE = "me.kavishdevar.librepods.SET_ANC_MODE"
|
||||||
|
private const val EXTRA_ANC_MODE = "anc_mode"
|
||||||
|
private const val ANIMATION_DURATION = 250L
|
||||||
|
|
||||||
|
private fun addAirPodsControlsToDialog(volumeDialog: Any) {
|
||||||
|
try {
|
||||||
|
val contextField = volumeDialog.javaClass.getDeclaredField("mContext")
|
||||||
|
contextField.isAccessible = true
|
||||||
|
val context = contextField.get(volumeDialog) as Context
|
||||||
|
|
||||||
|
val dialogViewField = volumeDialog.javaClass.getDeclaredField("mDialogView")
|
||||||
|
dialogViewField.isAccessible = true
|
||||||
|
val dialogView = dialogViewField.get(volumeDialog) as ViewGroup
|
||||||
|
|
||||||
|
val dialogRowsViewField = volumeDialog.javaClass.getDeclaredField("mDialogRowsView")
|
||||||
|
dialogRowsViewField.isAccessible = true
|
||||||
|
val dialogRowsView = dialogRowsViewField.get(volumeDialog) as ViewGroup
|
||||||
|
|
||||||
|
Log.d(TAG, "Found dialogRowsView: ${dialogRowsView.javaClass.name}")
|
||||||
|
|
||||||
|
val existingContainer = dialogView.findViewWithTag<View>("airpods_container")
|
||||||
|
if (existingContainer != null) {
|
||||||
|
Log.d(TAG, "AirPods container already exists, ensuring visibility state")
|
||||||
|
val drawer = existingContainer.findViewWithTag<View>("airpods_drawer_container")
|
||||||
|
drawer?.visibility = View.GONE
|
||||||
|
drawer?.alpha = 0f
|
||||||
|
drawer?.translationY = 0f
|
||||||
|
val button = existingContainer.findViewWithTag<ImageButton>("airpods_button")
|
||||||
|
button?.visibility = View.VISIBLE
|
||||||
|
button?.alpha = 1f
|
||||||
|
if (button != null) {
|
||||||
|
updateMainButtonIcon(context, button, currentANCMode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newAirPodsButton = ImageButton(context).apply {
|
||||||
|
tag = "airpods_button"
|
||||||
|
|
||||||
|
try {
|
||||||
|
val airPodsPackage = context.createPackageContext(
|
||||||
|
"me.kavishdevar.librepods",
|
||||||
|
Context.CONTEXT_IGNORE_SECURITY
|
||||||
|
)
|
||||||
|
val airPodsIconRes = airPodsPackage.resources.getIdentifier(
|
||||||
|
"airpods", "drawable", "me.kavishdevar.librepods")
|
||||||
|
|
||||||
|
if (airPodsIconRes != 0) {
|
||||||
|
val airPodsDrawable = airPodsPackage.resources.getDrawable(
|
||||||
|
airPodsIconRes, airPodsPackage.theme)
|
||||||
|
setImageDrawable(airPodsDrawable)
|
||||||
|
} else {
|
||||||
|
setImageResource(android.R.drawable.ic_media_play)
|
||||||
|
Log.d(TAG, "Using fallback icon because airpods icon resource not found")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setImageResource(android.R.drawable.ic_media_play)
|
||||||
|
Log.e(TAG, "Failed to load AirPods icon: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val shape = GradientDrawable()
|
||||||
|
shape.shape = GradientDrawable.RECTANGLE
|
||||||
|
shape.setColor(Color.BLACK)
|
||||||
|
background = shape
|
||||||
|
|
||||||
|
imageTintList = ColorStateList.valueOf(Color.WHITE)
|
||||||
|
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||||
|
|
||||||
|
setPadding(24, 24, 24, 24)
|
||||||
|
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
90
|
||||||
|
)
|
||||||
|
params.gravity = Gravity.CENTER
|
||||||
|
params.setMargins(0, 0, 0, 0)
|
||||||
|
layoutParams = params
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
Log.d(TAG, "AirPods button clicked, toggling drawer")
|
||||||
|
val container = findAirPodsContainer(this)
|
||||||
|
val drawerContainer = container?.findViewWithTag<View>("airpods_drawer_container")
|
||||||
|
if (drawerContainer != null && container != null) {
|
||||||
|
if (drawerContainer.visibility == View.VISIBLE) {
|
||||||
|
hideAirPodsDrawer(container, this, drawerContainer)
|
||||||
|
} else {
|
||||||
|
showAirPodsDrawer(container, this, drawerContainer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Could not find container or drawer for toggle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDescription = "AirPods Settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
val airPodsContainer = FrameLayout(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
tag = "airpods_container"
|
||||||
|
}
|
||||||
|
|
||||||
|
newAirPodsButton.setOnLongClickListener {
|
||||||
|
Log.d(TAG, "AirPods button long-pressed, opening QuickSettingsDialogActivity")
|
||||||
|
val intent = Intent().apply {
|
||||||
|
setClassName("me.kavishdevar.librepods", "me.kavishdevar.librepods.QuickSettingsDialogActivity")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
try {
|
||||||
|
val dismissMethod = volumeDialog.javaClass.getMethod("dismissH")
|
||||||
|
dismissMethod.invoke(volumeDialog)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Could not dismiss volume dialog: ${e.message}")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
val airPodsDrawer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
gravity = Gravity.TOP
|
||||||
|
}
|
||||||
|
tag = "airpods_drawer_container"
|
||||||
|
visibility = View.GONE
|
||||||
|
alpha = 0f
|
||||||
|
|
||||||
|
val drawerShape = GradientDrawable()
|
||||||
|
drawerShape.shape = GradientDrawable.RECTANGLE
|
||||||
|
drawerShape.setColor(Color.BLACK)
|
||||||
|
background = drawerShape
|
||||||
|
|
||||||
|
setPadding(16, 8, 16, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
val buttonContainer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
gravity = Gravity.TOP
|
||||||
|
}
|
||||||
|
tag = "airpods_button_container"
|
||||||
|
}
|
||||||
|
|
||||||
|
val modes = listOf(ANC_MODE_OFF, ANC_MODE_TRANSPARENCY, ANC_MODE_ADAPTIVE, ANC_MODE_NOISE_CANCELLATION)
|
||||||
|
for (mode in modes) {
|
||||||
|
val modeOption = createAncModeOption(context, mode, mode == currentANCMode, newAirPodsButton)
|
||||||
|
airPodsDrawer.addView(modeOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonContainer.addView(newAirPodsButton)
|
||||||
|
|
||||||
|
airPodsContainer.addView(airPodsDrawer)
|
||||||
|
airPodsContainer.addView(buttonContainer)
|
||||||
|
|
||||||
|
val settingsViewField = try {
|
||||||
|
val field = volumeDialog.javaClass.getDeclaredField("mSettingsView")
|
||||||
|
field.isAccessible = true
|
||||||
|
field.get(volumeDialog) as? View
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get settings view field: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsViewField != null && settingsViewField.parent is ViewGroup) {
|
||||||
|
val settingsParent = settingsViewField.parent as ViewGroup
|
||||||
|
val settingsIndex = findViewIndexInParent(settingsParent, settingsViewField)
|
||||||
|
|
||||||
|
if (settingsIndex >= 0) {
|
||||||
|
settingsParent.addView(airPodsContainer, settingsIndex)
|
||||||
|
Log.i(TAG, "Added AirPods controls before settings button")
|
||||||
|
} else {
|
||||||
|
settingsParent.addView(airPodsContainer)
|
||||||
|
Log.i(TAG, "Added AirPods controls to the end of settings parent")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialogView.addView(airPodsContainer)
|
||||||
|
Log.i(TAG, "Fallback: Added AirPods controls to dialog view")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMainButtonIcon(context, newAirPodsButton, currentANCMode)
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully added AirPods button and drawer to volume dialog")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error adding AirPods button to volume panel: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findViewIndexInParent(parent: ViewGroup, view: View): Int {
|
||||||
|
for (i in 0 until parent.childCount) {
|
||||||
|
if (parent.getChildAt(i) == view) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMainButtonIcon(context: Context, button: ImageButton, mode: Int) {
|
||||||
|
try {
|
||||||
|
val pkgContext = context.createPackageContext(
|
||||||
|
"me.kavishdevar.librepods",
|
||||||
|
Context.CONTEXT_IGNORE_SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
|
val resName = when (mode) {
|
||||||
|
ANC_MODE_OFF -> "noise_cancellation"
|
||||||
|
ANC_MODE_TRANSPARENCY -> "transparency"
|
||||||
|
ANC_MODE_ADAPTIVE -> "adaptive"
|
||||||
|
ANC_MODE_NOISE_CANCELLATION -> "noise_cancellation"
|
||||||
|
else -> "noise_cancellation"
|
||||||
|
}
|
||||||
|
|
||||||
|
val resId = pkgContext.resources.getIdentifier(
|
||||||
|
resName, "drawable", "me.kavishdevar.librepods"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (resId != 0) {
|
||||||
|
val drawable = pkgContext.resources.getDrawable(resId, pkgContext.theme)
|
||||||
|
button.setImageDrawable(drawable)
|
||||||
|
button.setColorFilter(Color.WHITE)
|
||||||
|
} else {
|
||||||
|
button.setImageResource(getIconResourceForMode(mode))
|
||||||
|
button.setColorFilter(Color.WHITE)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
button.setImageResource(getIconResourceForMode(mode))
|
||||||
|
button.setColorFilter(Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAncModeOption(context: Context, mode: Int, isSelected: Boolean, mainButton: ImageButton): LinearLayout {
|
||||||
|
return LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
setMargins(0, 6, 0, 6)
|
||||||
|
}
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
setPadding(24, 16, 24, 16)
|
||||||
|
tag = "anc_mode_${mode}"
|
||||||
|
|
||||||
|
val icon = ImageView(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(60, 60).apply {
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
}
|
||||||
|
tag = "mode_icon_$mode"
|
||||||
|
|
||||||
|
try {
|
||||||
|
val packageContext = context.createPackageContext(
|
||||||
|
"me.kavishdevar.librepods",
|
||||||
|
Context.CONTEXT_IGNORE_SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
|
val resourceName = when (mode) {
|
||||||
|
ANC_MODE_OFF -> "noise_cancellation"
|
||||||
|
ANC_MODE_TRANSPARENCY -> "transparency"
|
||||||
|
ANC_MODE_ADAPTIVE -> "adaptive"
|
||||||
|
ANC_MODE_NOISE_CANCELLATION -> "noise_cancellation"
|
||||||
|
else -> "noise_cancellation"
|
||||||
|
}
|
||||||
|
|
||||||
|
val resourceId = packageContext.resources.getIdentifier(
|
||||||
|
resourceName, "drawable", "me.kavishdevar.librepods"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (resourceId != 0) {
|
||||||
|
val drawable = packageContext.resources.getDrawable(
|
||||||
|
resourceId, packageContext.theme
|
||||||
|
)
|
||||||
|
setImageDrawable(drawable)
|
||||||
|
} else {
|
||||||
|
setImageResource(getIconResourceForMode(mode))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setImageResource(getIconResourceForMode(mode))
|
||||||
|
Log.e(TAG, "Failed to load custom drawable for mode $mode: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
setColorFilter(Color.BLACK)
|
||||||
|
} else {
|
||||||
|
setColorFilter(Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(icon)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
background = createSelectedBackground(context)
|
||||||
|
} else {
|
||||||
|
background = null
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
Log.d(TAG, "ANC mode selected: $mode (was: $currentANCMode)")
|
||||||
|
val container = findAirPodsContainer(this)
|
||||||
|
val drawerContainer = container?.findViewWithTag<View>("airpods_drawer_container")
|
||||||
|
|
||||||
|
if (currentANCMode == mode) {
|
||||||
|
if (drawerContainer != null && container != null) {
|
||||||
|
hideAirPodsDrawer(container, mainButton, drawerContainer)
|
||||||
|
}
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
currentANCMode = mode
|
||||||
|
|
||||||
|
val parentDrawer = parent as? ViewGroup
|
||||||
|
if (parentDrawer != null) {
|
||||||
|
for (i in 0 until parentDrawer.childCount) {
|
||||||
|
val child = parentDrawer.getChildAt(i) as? LinearLayout
|
||||||
|
if (child != null && child.tag.toString().startsWith("anc_mode_")) {
|
||||||
|
val childModeStr = child.tag.toString().substringAfter("anc_mode_")
|
||||||
|
val childMode = childModeStr.toIntOrNull() ?: -1
|
||||||
|
val childIcon = child.findViewWithTag<ImageView>("mode_icon_${childMode}")
|
||||||
|
|
||||||
|
if (childMode == mode) {
|
||||||
|
child.background = createSelectedBackground(context)
|
||||||
|
childIcon?.setColorFilter(Color.BLACK)
|
||||||
|
} else {
|
||||||
|
child.background = null
|
||||||
|
childIcon?.setColorFilter(Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(ACTION_SET_ANC_MODE).apply {
|
||||||
|
setPackage("me.kavishdevar.librepods")
|
||||||
|
putExtra(EXTRA_ANC_MODE, mode)
|
||||||
|
}
|
||||||
|
context.sendBroadcast(intent)
|
||||||
|
Log.d(TAG, "Sent broadcast to change ANC mode to: ${getLabelForMode(currentANCMode)}")
|
||||||
|
|
||||||
|
|
||||||
|
updateMainButtonIcon(context, mainButton, mode)
|
||||||
|
|
||||||
|
if (drawerContainer != null && container != null) {
|
||||||
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||||
|
hideAirPodsDrawer(container, mainButton, drawerContainer)
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSelectedBackground(context: Context): GradientDrawable {
|
||||||
|
return GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
setColor(Color.WHITE)
|
||||||
|
cornerRadius = 50f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findAirPodsContainer(view: View): ViewGroup? {
|
||||||
|
var current: View? = view
|
||||||
|
while (current != null) {
|
||||||
|
if (current is ViewGroup && current.tag == "airpods_container") {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
val parent = current.parent
|
||||||
|
if (parent is ViewGroup && parent.tag == "airpods_container") {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
current = parent as? View
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Could not find airpods_container ancestor")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAirPodsDrawer(container: ViewGroup, mainButton: ImageButton, drawerContainer: View) {
|
||||||
|
Log.d(TAG, "Showing AirPods drawer")
|
||||||
|
val selectedModeView = drawerContainer.findViewWithTag<View>("anc_mode_$currentANCMode")
|
||||||
|
val selectedModeIcon = selectedModeView?.findViewWithTag<ImageView>("mode_icon_$currentANCMode")
|
||||||
|
val buttonContainer = container.findViewWithTag<View>("airpods_button_container")
|
||||||
|
|
||||||
|
if (selectedModeView == null || selectedModeIcon == null) {
|
||||||
|
Log.e(TAG, "Cannot find selected mode view or icon for show animation")
|
||||||
|
|
||||||
|
drawerContainer.alpha = 0f
|
||||||
|
drawerContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
drawerContainer.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
buttonContainer?.animate()
|
||||||
|
?.alpha(0f)
|
||||||
|
?.setDuration(ANIMATION_DURATION / 2)
|
||||||
|
?.setStartDelay(ANIMATION_DURATION / 2)
|
||||||
|
?.withEndAction {
|
||||||
|
buttonContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
?.start()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerContainer.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(container.width, View.MeasureSpec.EXACTLY),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
)
|
||||||
|
|
||||||
|
val drawerHeight = drawerContainer.measuredHeight
|
||||||
|
|
||||||
|
drawerContainer.alpha = 0f
|
||||||
|
drawerContainer.visibility = View.VISIBLE
|
||||||
|
drawerContainer.translationY = -drawerHeight.toFloat()
|
||||||
|
|
||||||
|
drawerContainer.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(DecelerateInterpolator())
|
||||||
|
.start()
|
||||||
|
|
||||||
|
buttonContainer?.animate()
|
||||||
|
?.alpha(0f)
|
||||||
|
?.setDuration(ANIMATION_DURATION / 2)
|
||||||
|
?.setStartDelay(ANIMATION_DURATION / 3)
|
||||||
|
?.withEndAction {
|
||||||
|
buttonContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideAirPodsDrawer(container: ViewGroup, mainButton: ImageButton, drawerContainer: View) {
|
||||||
|
Log.d(TAG, "Hiding AirPods drawer")
|
||||||
|
val buttonContainer = container.findViewWithTag<View>("airpods_button_container")
|
||||||
|
|
||||||
|
if (buttonContainer != null && buttonContainer.visibility != View.VISIBLE) {
|
||||||
|
buttonContainer.alpha = 0f
|
||||||
|
buttonContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonContainer?.animate()
|
||||||
|
?.alpha(1f)
|
||||||
|
?.setDuration(ANIMATION_DURATION / 2)
|
||||||
|
?.start()
|
||||||
|
|
||||||
|
drawerContainer.animate()
|
||||||
|
.translationY(-drawerContainer.height.toFloat())
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(AccelerateInterpolator())
|
||||||
|
.setStartDelay(ANIMATION_DURATION / 4)
|
||||||
|
.withEndAction {
|
||||||
|
drawerContainer.visibility = View.GONE
|
||||||
|
drawerContainer.translationY = 0f
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIconResourceForMode(mode: Int): Int {
|
||||||
|
return when (mode) {
|
||||||
|
ANC_MODE_OFF -> android.R.drawable.ic_lock_silent_mode
|
||||||
|
ANC_MODE_TRANSPARENCY -> android.R.drawable.ic_lock_silent_mode_off
|
||||||
|
ANC_MODE_ADAPTIVE -> android.R.drawable.ic_menu_compass
|
||||||
|
ANC_MODE_NOISE_CANCELLATION -> android.R.drawable.ic_lock_idle_charging
|
||||||
|
else -> android.R.drawable.ic_lock_silent_mode_off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLabelForMode(mode: Int): String {
|
||||||
|
return when (mode) {
|
||||||
|
ANC_MODE_OFF -> "Off"
|
||||||
|
ANC_MODE_TRANSPARENCY -> "Transparency"
|
||||||
|
ANC_MODE_ADAPTIVE -> "Adaptive"
|
||||||
|
ANC_MODE_NOISE_CANCELLATION -> "Noise Cancellation"
|
||||||
|
else -> "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
@@ -25,7 +25,7 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
object MediaController {
|
object MediaController {
|
||||||
private var initialVolume: Int? = null
|
private var initialVolume: Int? = null
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
@file:Suppress("unused")
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -89,15 +89,15 @@ enum class NoiseControlMode {
|
|||||||
|
|
||||||
class AirPodsNotifications {
|
class AirPodsNotifications {
|
||||||
companion object {
|
companion object {
|
||||||
const val AIRPODS_CONNECTED = "me.kavishdevar.aln.AIRPODS_CONNECTED"
|
const val AIRPODS_CONNECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTED"
|
||||||
const val AIRPODS_DATA = "me.kavishdevar.aln.AIRPODS_DATA"
|
const val AIRPODS_DATA = "me.kavishdevar.librepods.AIRPODS_DATA"
|
||||||
const val EAR_DETECTION_DATA = "me.kavishdevar.aln.EAR_DETECTION_DATA"
|
const val EAR_DETECTION_DATA = "me.kavishdevar.librepods.EAR_DETECTION_DATA"
|
||||||
const val ANC_DATA = "me.kavishdevar.aln.ANC_DATA"
|
const val ANC_DATA = "me.kavishdevar.librepods.ANC_DATA"
|
||||||
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
|
const val BATTERY_DATA = "me.kavishdevar.librepods.BATTERY_DATA"
|
||||||
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
|
const val CA_DATA = "me.kavishdevar.librepods.CA_DATA"
|
||||||
const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED"
|
const val AIRPODS_DISCONNECTED = "me.kavishdevar.librepods.AIRPODS_DISCONNECTED"
|
||||||
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.aln.AIRPODS_CONNECTION_DETECTED"
|
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTION_DETECTED"
|
||||||
const val DISCONNECT_RECEIVERS = "me.kavishdevar.aln.DISCONNECT_RECEIVERS"
|
const val DISCONNECT_RECEIVERS = "me.kavishdevar.librepods.DISCONNECT_RECEIVERS"
|
||||||
}
|
}
|
||||||
|
|
||||||
class EarDetection {
|
class EarDetection {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -17,14 +17,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
|
import android.animation.PropertyValuesHolder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -37,15 +40,17 @@ import android.widget.ImageButton
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.VideoView
|
import android.widget.VideoView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import me.kavishdevar.librepods.R
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.kavishdevar.aln.R
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams", "ClickableViewAccessibility")
|
@SuppressLint("InflateParams", "ClickableViewAccessibility")
|
||||||
class PopupWindow(context: Context) {
|
class PopupWindow(
|
||||||
|
private val context: Context,
|
||||||
|
private val onCloseCallback: () -> Unit = {}
|
||||||
|
) {
|
||||||
private val mView: View
|
private val mView: View
|
||||||
|
private var isClosing = false
|
||||||
|
private var autoCloseHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var autoCloseRunnable: Runnable? = null
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
|
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
|
||||||
@@ -109,76 +114,117 @@ class PopupWindow(context: Context) {
|
|||||||
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi", "SetTextI18n")
|
@SuppressLint("InlinedApi", "SetTextI18s")
|
||||||
fun open(name: String = "AirPods Pro", batteryNotification: AirPodsNotifications.BatteryNotification) {
|
fun open(name: String = "AirPods Pro", batteryNotification: AirPodsNotifications.BatteryNotification) {
|
||||||
try {
|
try {
|
||||||
if (mView.windowToken == null) {
|
if (mView.windowToken == null && mView.parent == null && !isClosing) {
|
||||||
if (mView.parent == null) {
|
|
||||||
mWindowManager.addView(mView, mParams)
|
|
||||||
mView.findViewById<TextView>(R.id.name).text = name
|
mView.findViewById<TextView>(R.id.name).text = name
|
||||||
val vid = mView.findViewById<VideoView>(R.id.video)
|
|
||||||
|
|
||||||
vid.setVideoPath("android.resource://me.kavishdevar.aln/" + R.raw.connected)
|
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)
|
vid.resolveAdjustedSize(vid.width, vid.height)
|
||||||
vid.start()
|
vid.start()
|
||||||
vid.setOnCompletionListener {
|
vid.setOnCompletionListener {
|
||||||
vid.start()
|
vid.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
val batteryStatus = batteryNotification.getBattery()
|
mWindowManager.addView(mView, mParams)
|
||||||
val batteryLeftText = mView.findViewById<TextView>(R.id.left_battery)
|
|
||||||
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
|
|
||||||
val batteryCaseText = mView.findViewById<TextView>(R.id.case_battery)
|
|
||||||
|
|
||||||
batteryLeftText.text = batteryStatus.find { it.component == BatteryComponent.LEFT }?.let {
|
|
||||||
"\uDBC3\uDC8E ${it.level}%"
|
|
||||||
} ?: ""
|
|
||||||
batteryRightText.text = batteryStatus.find { it.component == BatteryComponent.RIGHT }?.let {
|
|
||||||
"\uDBC3\uDC8D ${it.level}%"
|
|
||||||
} ?: ""
|
|
||||||
batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let {
|
|
||||||
"\uDBC3\uDE6C ${it.level}%"
|
|
||||||
} ?: ""
|
|
||||||
|
|
||||||
val displayMetrics = mView.context.resources.displayMetrics
|
val displayMetrics = mView.context.resources.displayMetrics
|
||||||
val screenHeight = displayMetrics.heightPixels
|
val screenHeight = displayMetrics.heightPixels
|
||||||
|
|
||||||
mView.translationY = screenHeight.toFloat()
|
mView.translationY = screenHeight.toFloat()
|
||||||
ObjectAnimator.ofFloat(mView, "translationY", 0f).apply {
|
mView.alpha = 1f
|
||||||
|
|
||||||
|
val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, screenHeight.toFloat(), 0f)
|
||||||
|
|
||||||
|
ObjectAnimator.ofPropertyValuesHolder(mView, translationY).apply {
|
||||||
duration = 500
|
duration = 500
|
||||||
interpolator = DecelerateInterpolator()
|
interpolator = DecelerateInterpolator()
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(MainScope().coroutineContext).launch {
|
autoCloseRunnable = Runnable { close() }
|
||||||
delay(12000)
|
autoCloseHandler.postDelayed(autoCloseRunnable!!, 12000)
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("PopupService", e.toString())
|
Log.e("PopupWindow", "Error opening popup: ${e.message}")
|
||||||
|
onCloseCallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
fun updateBatteryStatus(batteryNotification: AirPodsNotifications.BatteryNotification) {
|
||||||
|
val batteryStatus = batteryNotification.getBattery()
|
||||||
|
|
||||||
|
val batteryLeftText = mView.findViewById<TextView>(R.id.left_battery)
|
||||||
|
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
|
||||||
|
val batteryCaseText = mView.findViewById<TextView>(R.id.case_battery)
|
||||||
|
|
||||||
|
batteryLeftText.text = batteryStatus.find { it.component == BatteryComponent.LEFT }?.let {
|
||||||
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
|
"\uDBC3\uDC8E ${it.level}%"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
batteryRightText.text = batteryStatus.find { it.component == BatteryComponent.RIGHT }?.let {
|
||||||
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
|
"\uDBC3\uDC8D ${it.level}%"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let {
|
||||||
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
|
"\uDBC3\uDE6C ${it.level}%"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
try {
|
try {
|
||||||
|
if (isClosing) return
|
||||||
|
isClosing = true
|
||||||
|
|
||||||
|
autoCloseRunnable?.let { autoCloseHandler.removeCallbacks(it) }
|
||||||
|
|
||||||
|
val vid = mView.findViewById<VideoView>(R.id.video)
|
||||||
|
vid.stopPlayback()
|
||||||
|
|
||||||
ObjectAnimator.ofFloat(mView, "translationY", mView.height.toFloat()).apply {
|
ObjectAnimator.ofFloat(mView, "translationY", mView.height.toFloat()).apply {
|
||||||
duration = 500
|
duration = 500
|
||||||
interpolator = AccelerateInterpolator()
|
interpolator = AccelerateInterpolator()
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
try {
|
try {
|
||||||
|
mView.visibility = View.GONE
|
||||||
|
if (mView.parent != null) {
|
||||||
mWindowManager.removeView(mView)
|
mWindowManager.removeView(mView)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("PopupService", e.toString())
|
Log.e("PopupWindow", "Error removing view: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
isClosing = false
|
||||||
|
onCloseCallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("PopupService", e.toString())
|
Log.e("PopupWindow", "Error closing popup: ${e.message}")
|
||||||
|
isClosing = false
|
||||||
|
onCloseCallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isShowing: Boolean
|
||||||
|
get() = mView.parent != null && !isClosing
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.aln.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -36,9 +37,11 @@ import java.net.URL
|
|||||||
class RadareOffsetFinder(context: Context) {
|
class RadareOffsetFinder(context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RadareOffsetFinder"
|
private const val TAG = "RadareOffsetFinder"
|
||||||
// Custom static build of radare2 for Android that doesn't need Termux. See: https://github.com/devnoname120/radare2/releases/tag/5.9.8-android-aln
|
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 RADARE2_URL = "https://github.com/devnoname120/radare2/releases/download/5.9.8-android-aln/radare2-5.9.9-android-aarch64-aln.tar.gz"
|
private const val HOOK_OFFSET_PROP = "persist.librepods.hook_offset"
|
||||||
private const val HOOK_OFFSET_PROP = "persist.aln.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 EXTRACT_DIR = "/"
|
||||||
|
|
||||||
private const val RADARE2_BIN_PATH = "$EXTRACT_DIR/data/local/tmp/aln_unzip/org.radare.radare2installer/radare2/bin"
|
private const val RADARE2_BIN_PATH = "$EXTRACT_DIR/data/local/tmp/aln_unzip/org.radare.radare2installer/radare2/bin"
|
||||||
@@ -64,21 +67,25 @@ class RadareOffsetFinder(context: Context) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearHookOffset(): Boolean {
|
fun clearHookOffsets(): Boolean {
|
||||||
try {
|
try {
|
||||||
val process = Runtime.getRuntime().exec(arrayOf(
|
val process = Runtime.getRuntime().exec(arrayOf(
|
||||||
"su", "-c", "setprop $HOOK_OFFSET_PROP ''"
|
"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()
|
val exitCode = process.waitFor()
|
||||||
|
|
||||||
if (exitCode == 0) {
|
if (exitCode == 0) {
|
||||||
Log.d(TAG, "Successfully cleared hook offset property")
|
Log.d(TAG, "Successfully cleared hook offset properties")
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to clear hook offset property, exit code: $exitCode")
|
Log.e(TAG, "Failed to clear hook offset properties, exit code: $exitCode")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error clearing hook offset property", e)
|
Log.e(TAG, "Error clearing hook offset properties", e)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -105,6 +112,11 @@ class RadareOffsetFinder(context: Context) {
|
|||||||
|
|
||||||
|
|
||||||
fun isHookOffsetAvailable(): Boolean {
|
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
|
_progressState.value = ProgressState.CheckingExisting
|
||||||
try {
|
try {
|
||||||
val process = Runtime.getRuntime().exec(arrayOf("getprop", HOOK_OFFSET_PROP))
|
val process = Runtime.getRuntime().exec(arrayOf("getprop", HOOK_OFFSET_PROP))
|
||||||
@@ -404,6 +416,10 @@ class RadareOffsetFinder(context: Context) {
|
|||||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
||||||
|
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
||||||
|
findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to find function offset", e)
|
Log.e(TAG, "Failed to find function offset", e)
|
||||||
return@withContext 0L
|
return@withContext 0L
|
||||||
@@ -418,6 +434,141 @@ class RadareOffsetFinder(context: Context) {
|
|||||||
return@withContext offset
|
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) {
|
private suspend fun saveOffset(offset: Long): Boolean = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val hexString = "0x${offset.toString(16)}"
|
val hexString = "0x${offset.toString(16)}"
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
|
||||||
|
object SystemApisUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device type which is used in METADATA_DEVICE_TYPE
|
||||||
|
* Indicates this Bluetooth device is an untethered headset.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET: String
|
||||||
|
get() = "Untethered Headset"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum length of a metadata entry, this is to avoid exploding Bluetooth
|
||||||
|
* disk usage
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MAX_LENGTH: Int
|
||||||
|
get() = 2048
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manufacturer name of this Bluetooth device
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MANUFACTURER_NAME: Int
|
||||||
|
get() = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model name of this Bluetooth device
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MODEL_NAME: Int
|
||||||
|
get() = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Software version of this Bluetooth device
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_SOFTWARE_VERSION: Int
|
||||||
|
get() = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hardware version of this Bluetooth device
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_HARDWARE_VERSION: Int
|
||||||
|
get() = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package name of the companion app, if any
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_COMPANION_APP: Int
|
||||||
|
get() = 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI to the main icon shown on the settings UI
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MAIN_ICON: Int
|
||||||
|
get() = 5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this device is an untethered headset with left, right and case
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET: Int
|
||||||
|
get() = 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI to icon of the left headset
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON: Int
|
||||||
|
get() = 7
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI to icon of the right headset
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON: Int
|
||||||
|
get() = 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI to icon of the headset charging case
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_CASE_ICON: Int
|
||||||
|
get() = 9
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Battery level of left headset
|
||||||
|
* Data type should be {@String} 0-100 as [Byte] array, otherwise
|
||||||
|
* as invalid.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY: Int
|
||||||
|
get() = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Battery level of rigth headset
|
||||||
|
* Data type should be {@String} 0-100 as [Byte] array, otherwise
|
||||||
|
* as invalid.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY: Int
|
||||||
|
get() = 11
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Battery level of the headset charging case
|
||||||
|
* Data type should be {@String} 0-100 as [Byte] array, otherwise
|
||||||
|
* as invalid.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY: Int
|
||||||
|
get() = 12
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the left headset is charging
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING: Int
|
||||||
|
get() = 13
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the right headset is charging
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING: Int
|
||||||
|
get() = 14
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the headset charging case is charging
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING: Int
|
||||||
|
get() = 15
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI to the enhanced settings UI slice
|
||||||
|
* Data type should be {@String} as [Byte] array, null means
|
||||||
|
* the UI does not exist.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI: Int
|
||||||
|
get() = 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.COMPANION_TYPE_PRIMARY: String
|
||||||
|
get() = "COMPANION_PRIMARY"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.COMPANION_TYPE_SECONDARY: String
|
||||||
|
get() = "COMPANION_SECONDARY"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.COMPANION_TYPE_NONE: String
|
||||||
|
get() = "COMPANION_NONE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the Bluetooth device, must be within the list of
|
||||||
|
* BluetoothDevice.DEVICE_TYPE_*
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_DEVICE_TYPE: Int
|
||||||
|
get() = 17
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Battery level of the Bluetooth device, use when the Bluetooth device
|
||||||
|
* does not support HFP battery indicator.
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MAIN_BATTERY: Int
|
||||||
|
get() = 18
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the device is charging.
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MAIN_CHARGING: Int
|
||||||
|
get() = 19
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The battery threshold of the Bluetooth device to show low battery icon.
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_MAIN_LOW_BATTERY_THRESHOLD: Int
|
||||||
|
get() = 20
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The battery threshold of the left headset to show low battery icon.
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD: Int
|
||||||
|
get() = 21
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The battery threshold of the right headset to show low battery icon.
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD: Int
|
||||||
|
get() = 22
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The battery threshold of the case to show low battery icon.
|
||||||
|
* Data type should be {@String} as [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD: Int
|
||||||
|
get() = 23
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata of the audio spatial data.
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_SPATIAL_AUDIO: Int
|
||||||
|
get() = 24
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata of the Fast Pair for any custmized feature.
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int
|
||||||
|
get() = 25
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata of the Fast Pair for LE Audio capable devices.
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_LE_AUDIO: Int
|
||||||
|
get() = 26
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UUIDs (16-bit) of registered to CCC characteristics from Media Control services.
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_GMCS_CCCD: Int
|
||||||
|
get() = 27
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UUIDs (16-bit) of registered to CCC characteristics from Telephony Bearer service.
|
||||||
|
* Data type should be [Byte] array.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
val BluetoothDevice.METADATA_GTBS_CCCD: Int
|
||||||
|
get() = 28
|
||||||
|
|
||||||
|
const val BATTERY_LEVEL_UNKNOWN: Int = -1
|
||||||
|
|
||||||
|
const val ACTION_BLUETOOTH_HANDSFREE_BATTERY_CHANGED = "android.intent.action.BLUETOOTH_HANDSFREE_BATTERY_CHANGED"
|
||||||
|
const val EXTRA_SHOW_BT_HANDSFREE_BATTERY = "android.intent.extra.show_bluetooth_handsfree_battery"
|
||||||
|
const val EXTRA_BT_HANDSFREE_BATTERY_LEVEL = "android.intent.extra.bluetooth_handsfree_battery_level"
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.aln.widgets
|
package me.kavishdevar.librepods.widgets
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
@@ -26,9 +26,9 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import me.kavishdevar.aln.MainActivity
|
import me.kavishdevar.librepods.MainActivity
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
class BatteryWidget : AppWidgetProvider() {
|
class BatteryWidget : AppWidgetProvider() {
|
||||||
override fun onUpdate(
|
override fun onUpdate(
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2024 Kavish Devar
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package me.kavishdevar.aln.widgets
|
package me.kavishdevar.librepods.widgets
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
@@ -25,8 +25,8 @@ import android.appwidget.AppWidgetProvider
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
class NoiseControlWidget : AppWidgetProvider() {
|
class NoiseControlWidget : AppWidgetProvider() {
|
||||||
override fun onUpdate(
|
override fun onUpdate(
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorBackground"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
||||||
@@ -4,27 +4,36 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
<group
|
||||||
|
android:translateX="13.5"
|
||||||
|
android:translateY="13.5"
|
||||||
|
android:scaleX="0.75"
|
||||||
|
android:scaleY="0.75">
|
||||||
|
<path
|
||||||
|
android:pathData="M30.07 66.68l-1.73-1.32c-1.35-1.18-2.58-2.49-3.68-3.9l-0.12-0.17-1.28-1.92-0.72-1.24-1.02-2.3c-0.52-1.4-0.9-2.86-1.1-4.34l-0.06-0.45-0.08-1.13-0.07-1.28 0.03-0.79c0-0.37 0.04-0.75 0.1-1.12l0.02-0.14 0.14-0.52c0.2-0.72 0.8-1.27 1.54-1.4l0.3-0.01c0.2-0.01 0.4 0 0.6 0.07 0.22 0.06 0.43 0.18 0.62 0.33l0.62 0.52L26.1 47c1.61 1.26 3.3 2.4 5.09 3.37 0.6 0.33 1.23 0.66 1.77 0.93 0.87 0.43 1.77 0.82 2.68 1.18l0.9 0.36 0.45 0.14c0.7 0.2 1.42 0.36 2.15 0.46l0.56 0.04h0.14c0.12 0 0.18-0.14 0.1-0.23l-0.04-0.02L39.5 53l-0.76-0.41-1.24-0.64-3.39-1.47-2.78-1.62-1.44-0.87-1.69-1.35-1.96-1.58-1.92-1.89-1.36-1.73-0.1-0.13c-0.88-1.16-1.64-2.42-2.27-3.75l-0.71-2.22-0.57-1.89c-0.12-0.57-0.2-1.16-0.22-1.75l-0.05-1.14L19 25.88l0.01-0.11c0.07-0.78 0.2-1.54 0.4-2.3l0.3-1.01 0.38-1.1 0.4-0.87c0.06-0.15 0.14-0.28 0.23-0.4l0.05-0.07c0.1-0.13 0.22-0.25 0.35-0.34 0.22-0.16 0.47-0.25 0.73-0.29h0.04c0.1-0.01 0.22-0.02 0.33-0.01l0.21 0.01c0.12 0.01 0.25 0.03 0.36 0.06h0.03c0.26 0.07 0.5 0.2 0.72 0.36 0.14 0.1 0.26 0.23 0.36 0.37l0.41 0.54 1.54 1.77 1.81 2.08c0.88 0.9 1.82 1.73 2.83 2.5l0.8 0.6 2.16 1.23c0.15 0.09 0.3 0.15 0.47 0.2l1.66 0.53c0.34 0.11 0.53 0.47 0.44 0.81l-0.05 0.12-0.11 0.23c-0.24 0.47-0.36 0.98-0.36 1.5v2.3c0 1.27 0.18 2.52 0.54 3.73l0.5 1.68c0.63 1.66 1.48 3.23 2.54 4.66l0.48 0.66c0.96 1.3 2.04 2.51 3.22 3.62l3.22 3 1.69 1.27c0.2 0.15 0.44 0.27 0.69 0.34l0.17 0.04c0.3 0.09 0.55 0.25 0.75 0.49 0.22 0.27 0.34 0.6 0.34 0.95v33c0 0.18 0.13 0.33 0.31 0.36h0.07-0.07l-5.87-0.51-2.37-0.45C40 87.1 38.32 86.63 36.7 86l-2.44-1.3-1.01-0.73c-0.87-0.62-1.67-1.31-2.41-2.07-0.71-0.72-1.36-1.5-1.93-2.32l-0.66-0.94-0.98-1.39c-0.48-0.67-0.84-1.4-1.1-2.18-0.17-0.56-0.28-1.14-0.33-1.72l-0.01-0.08c-0.04-0.46-0.04-0.92 0-1.38l0.06-0.97V70.9c0-0.71 0.08-1.41 0.23-2.1l0.23-0.68c0.09-0.24 0.23-0.45 0.41-0.62 0.17-0.16 0.38-0.28 0.6-0.34l0.18-0.05c0.37-0.11 0.76-0.1 1.12 0.03 0.14 0.05 0.28 0.11 0.4 0.2l1.04 0.69 2.21 1.62 2.67 1.86 1.94 1.17 1.77 0.95c0.5 0.27 1.04 0.5 1.58 0.7l1.97 0.73 0.12 0.02h0.1c0.11 0 0.21-0.1 0.23-0.21 0-0.1-0.04-0.19-0.12-0.23l-0.94-0.57-1.68-1.05-3.72-2.05-2.88-2.13-3.28-2.16Z">
|
||||||
<aapt:attr name="android:fillColor">
|
<aapt:attr name="android:fillColor">
|
||||||
<gradient
|
<gradient
|
||||||
android:endX="85.84757"
|
android:type="linear"
|
||||||
android:endY="92.4963"
|
android:startX="34.51"
|
||||||
android:startX="42.9492"
|
android:startY="19.37"
|
||||||
android:startY="49.59793"
|
android:endX="34.51"
|
||||||
android:type="linear">
|
android:endY="88.4">
|
||||||
<item
|
<item
|
||||||
android:color="#44000000"
|
android:color="#FF64AB5D"
|
||||||
android:offset="0.0" />
|
android:offset="0"/>
|
||||||
<item
|
<item
|
||||||
android:color="#00000000"
|
android:color="#FF21395B"
|
||||||
android:offset="1.0" />
|
android:offset="1"/>
|
||||||
</gradient>
|
</gradient>
|
||||||
</aapt:attr>
|
</aapt:attr>
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:strokeColor="@color/popup_text"
|
||||||
android:fillType="nonZero"
|
android:strokeWidth="0.5"
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
android:pathData="M30.07 66.68l-1.73-1.32c-1.35-1.18-2.58-2.49-3.68-3.9l-0.12-0.17-1.28-1.92-0.72-1.24-1.02-2.3c-0.52-1.4-0.9-2.86-1.1-4.34l-0.06-0.45-0.08-1.13-0.07-1.28 0.03-0.79c0-0.37 0.04-0.75 0.1-1.12l0.02-0.14 0.14-0.52c0.2-0.72 0.8-1.27 1.54-1.4l0.3-0.01c0.2-0.01 0.4 0 0.6 0.07 0.22 0.06 0.43 0.18 0.62 0.33l0.62 0.52L26.1 47c1.61 1.26 3.3 2.4 5.09 3.37 0.6 0.33 1.23 0.66 1.77 0.93 0.87 0.43 1.77 0.82 2.68 1.18l0.9 0.36 0.45 0.14c0.7 0.2 1.42 0.36 2.15 0.46l0.56 0.04h0.14c0.12 0 0.18-0.14 0.1-0.23l-0.04-0.02L39.5 53l-0.76-0.41-1.24-0.64-3.39-1.47-2.78-1.62-1.44-0.87-1.69-1.35-1.96-1.58-1.92-1.89-1.36-1.73-0.1-0.13c-0.88-1.16-1.64-2.42-2.27-3.75l-0.71-2.22-0.57-1.89c-0.12-0.57-0.2-1.16-0.22-1.75l-0.05-1.14L19 25.88l0.01-0.11c0.07-0.78 0.2-1.54 0.4-2.3l0.3-1.01 0.38-1.1 0.4-0.87c0.06-0.15 0.14-0.28 0.23-0.4l0.05-0.07c0.1-0.13 0.22-0.25 0.35-0.34 0.22-0.16 0.47-0.25 0.73-0.29h0.04c0.1-0.01 0.22-0.02 0.33-0.01l0.21 0.01c0.12 0.01 0.25 0.03 0.36 0.06h0.03c0.26 0.07 0.5 0.2 0.72 0.36 0.14 0.1 0.26 0.23 0.36 0.37l0.41 0.54 1.54 1.77 1.81 2.08c0.88 0.9 1.82 1.73 2.83 2.5l0.8 0.6 2.16 1.23c0.15 0.09 0.3 0.15 0.47 0.2l1.66 0.53c0.34 0.11 0.53 0.47 0.44 0.81l-0.05 0.12-0.11 0.23c-0.24 0.47-0.36 0.98-0.36 1.5v2.3c0 1.27 0.18 2.52 0.54 3.73l0.5 1.68c0.63 1.66 1.48 3.23 2.54 4.66l0.48 0.66c0.96 1.3 2.04 2.51 3.22 3.62l3.22 3 1.69 1.27c0.2 0.15 0.44 0.27 0.69 0.34l0.17 0.04c0.3 0.09 0.55 0.25 0.75 0.49 0.22 0.27 0.34 0.6 0.34 0.95v33c0 0.18 0.13 0.33 0.31 0.36h0.07-0.07l-5.87-0.51-2.37-0.45C40 87.1 38.32 86.63 36.7 86l-2.44-1.3-1.01-0.73c-0.87-0.62-1.67-1.31-2.41-2.07-0.71-0.72-1.36-1.5-1.93-2.32l-0.66-0.94-0.98-1.39c-0.48-0.67-0.84-1.4-1.1-2.18-0.17-0.56-0.28-1.14-0.33-1.72l-0.01-0.08c-0.04-0.46-0.04-0.92 0-1.38l0.06-0.97V70.9c0-0.71 0.08-1.41 0.23-2.1l0.23-0.68c0.09-0.24 0.23-0.45 0.41-0.62 0.17-0.16 0.38-0.28 0.6-0.34l0.18-0.05c0.37-0.11 0.76-0.1 1.12 0.03 0.14 0.05 0.28 0.11 0.4 0.2l1.04 0.69 2.21 1.62 2.67 1.86 1.94 1.17 1.77 0.95c0.5 0.27 1.04 0.5 1.58 0.7l1.97 0.73 0.12 0.02h0.1c0.11 0 0.21-0.1 0.23-0.21 0-0.1-0.04-0.19-0.12-0.23l-0.94-0.57-1.68-1.05-3.72-2.05-2.88-2.13-3.28-2.16Z"/>
|
||||||
android:strokeWidth="1"
|
<path
|
||||||
android:strokeColor="#00000000" />
|
android:strokeColor="@color/popup_text"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:pathData="M49.59 54.33v33.15 0.04c0 0.62 0.14 1.23 0.42 1.78m-0.42-34.97l-2.1-1.4-0.29-0.2c-1.67-1.17-3.26-2.46-4.75-3.86l-0.35-0.35c-1.54-1.53-2.88-3.24-4-5.1l-0.86-1.57c-0.45-0.82-0.82-1.68-1.1-2.57-0.46-1.43-0.7-2.92-0.7-4.42v-0.54-0.74c0-1.27 0.16-2.53 0.47-3.77 0.25-1 0.6-1.97 1.04-2.9l0.74-1.54 0.4-0.67c0.85-1.41 1.84-2.73 2.96-3.94l0.84-0.78c0.56-0.5 1.17-0.95 1.82-1.32l0.98-0.56 1.1-0.56 1.28-0.56 1-0.36c0.63-0.23 1.3-0.4 1.97-0.5 0.54-0.08 1.08-0.12 1.63-0.12h1.7 0.61c0.62 0 1.23 0.04 1.85 0.13 0.69 0.1 1.37 0.25 2.04 0.46l1.24 0.39 2.24 0.56 2.1 0.84 1.54 0.84 1.96 1.12 1.82 1.26 1.68 1.25 0.23 0.2c0.87 0.7 1.68 1.48 2.43 2.32l1.12 1.12 1.26 1.54 1.12 1.54 0.84 1.4 0.59 1.3M49.59 54.34l0.06 0.04c0.33 0.25 0.68 0.47 1.06 0.66l1.54 0.7 1.68 0.7 1.68 0.56 1.54 0.42 1.54 0.42 1.54 0.28 0.84 0.14 1.12 0.04 0.56-0.04h0.56m14.73-25.97l-1.01-0.05h-1.54-0.06c-0.8 0-1.58 0.14-2.32 0.42l-1.12 0.42-0.55 0.3c-0.66 0.35-1.28 0.78-1.86 1.26-0.54 0.45-1.04 0.94-1.49 1.48l-0.72 0.87-0.16 0.19c-0.83 1-1.57 2.05-2.22 3.17l-0.84 1.4-0.84 1.4-0.84 1.82-0.7 1.95-0.42 1.96-0.28 2.1v0.11c0 0.95 0.1 1.9 0.28 2.83l0.42 1.4c0.28 0.92 0.7 1.8 1.28 2.58l0.26 0.36m14.73-25.97l1.65 0.37c0.84 0.18 1.65 0.46 2.43 0.82l0.14 0.07c0.52 0.24 1.03 0.53 1.52 0.85l0.11 0.08c0.56 0.37 1.08 0.8 1.55 1.26 0.37 0.37 0.7 0.76 1 1.18l1.09 1.47c0.65 0.93 1.17 1.94 1.55 3l0.13 0.36 0.02 0.06c0.36 1.17 0.62 2.37 0.77 3.58v0.8c0 0.78-0.06 1.55-0.18 2.32-0.22 1.45-0.65 2.87-1.27 4.2l-0.13 0.27-0.89 1.64-1.54 2.1-0.42 0.5c-0.56 0.69-1.17 1.33-1.84 1.92l-0.06 0.05c-0.88 0.77-1.84 1.44-2.86 2l-0.25 0.11c-1.14 0.5-2.32 0.88-3.53 1.15l-0.17 0.04c-0.72 0.16-1.47 0.24-2.21 0.24h-0.79c-1.15 0-2.3-0.14-3.41-0.42l-1.82-0.7-1.68-0.7-0.62-0.33c-0.61-0.34-1.17-0.76-1.67-1.25-0.34-0.34-0.71-0.65-1.12-0.92l-0.23-0.15m0 0v1.81 0.84 1.4l-0.42 24.9v0.43c0 0.55-0.1 1.1-0.28 1.61M50 89.3l0.3 0.52c0.17 0.3 0.38 0.58 0.63 0.83 0.4 0.4 0.88 0.71 1.4 0.9l0.33 0.13 0.1 0.04c0.29 0.11 0.58 0.2 0.88 0.26 0.37 0.08 0.75 0.12 1.13 0.12h0.55 0.84H57h0.7c0.37 0 0.75-0.05 1.11-0.14l0.3-0.07c0.56-0.14 1.1-0.35 1.62-0.6l0.05-0.03 0.42-0.28 0.28-0.18c0.18-0.12 0.35-0.26 0.5-0.42 0.22-0.25 0.4-0.54 0.51-0.86l0.1-0.28M50 89.3l12.6-0.06M48.9 31.81l-0.86-0.65c-0.17-0.13-0.32-0.28-0.45-0.47-0.06-0.1-0.12-0.2-0.16-0.3l-0.14-0.32c-0.14-0.33-0.21-0.69-0.21-1.05 0-0.28 0.04-0.57 0.13-0.84L47.26 28l0.2-0.5 0.16-0.3c0.1-0.2 0.23-0.38 0.39-0.54 0.12-0.12 0.25-0.22 0.4-0.31l0.29-0.17c0.12-0.08 0.26-0.15 0.4-0.2l0.11-0.05c0.25-0.1 0.51-0.15 0.78-0.15 0.2 0 0.4 0.03 0.58 0.08l0.26 0.08c0.3 0.08 0.57 0.2 0.83 0.34l1.15 0.62 1.4 0.84 1.12 0.84 1.4 0.98 1.17 0.89 0.3 0.28c0.14 0.15 0.25 0.32 0.34 0.5l0.02 0.03c0.18 0.35 0.27 0.74 0.27 1.13v0.25 0.19c0 0.42-0.1 0.84-0.3 1.22-0.08 0.18-0.18 0.34-0.3 0.5l-0.1 0.12c-0.19 0.23-0.42 0.41-0.68 0.54-0.1 0.06-0.22 0.1-0.34 0.14l-0.21 0.06c-0.4 0.1-0.8 0.16-1.2 0.16h-0.37c-0.28 0-0.55-0.05-0.81-0.15l-0.12-0.05c-0.13-0.05-0.25-0.11-0.37-0.18l-1.5-0.88-1.82-1.25-1.82-1.26Zm36.96 17.06l0.06-0.42c0.05-0.37 0.05-0.74-0.01-1.11l-0.01-0.07c-0.03-0.14-0.06-0.29-0.11-0.43l-0.1-0.28c-0.07-0.23-0.2-0.43-0.37-0.6l-0.07-0.07c-0.15-0.15-0.34-0.26-0.55-0.31-0.16-0.04-0.32-0.05-0.48-0.02l-0.25 0.04c-0.23 0.04-0.46 0.1-0.67 0.22l-0.14 0.07c-0.25 0.12-0.49 0.28-0.7 0.46l-0.26 0.22c-0.27 0.23-0.51 0.48-0.74 0.75l-0.36 0.43-0.56 0.84-0.84 1.26-0.14 0.21c-0.28 0.42-0.51 0.87-0.7 1.33l-0.56 1.54-0.1 0.36c-0.12 0.4-0.18 0.84-0.18 1.27v0.39c0 0.24 0.04 0.47 0.11 0.7l0.08 0.22c0.06 0.18 0.16 0.35 0.3 0.49l0.05 0.05c0.1 0.1 0.23 0.18 0.37 0.23 0.14 0.04 0.28 0.06 0.43 0.04l0.36-0.06c0.26-0.03 0.52-0.11 0.76-0.23l0.44-0.22c0.4-0.2 0.77-0.45 1.11-0.74l0.47-0.4 0.03-0.04c0.73-0.81 1.37-1.69 1.93-2.62 0.37-0.65 0.69-1.33 0.95-2.04l0.17-0.48 0.28-0.98Z"/>
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group
|
||||||
|
android:translateX="21.6"
|
||||||
|
android:translateY="21.6"
|
||||||
|
android:scaleX="0.6"
|
||||||
|
android:scaleY="0.6">
|
||||||
|
<group>
|
||||||
|
<path
|
||||||
|
android:strokeColor="@color/popup_text"
|
||||||
|
android:strokeWidth="3.25"
|
||||||
|
android:pathData="M49.64 54.5v33.52c0 0.21 0.17 0.38 0.38 0.38l-5.94-0.52-2.37-0.45C40 87.1 38.32 86.63 36.7 86l-2.44-1.3-1.01-0.73c-0.87-0.62-1.67-1.31-2.41-2.07-0.71-0.72-1.36-1.5-1.93-2.32l-0.66-0.94-0.98-1.39c-0.48-0.67-0.84-1.4-1.1-2.18-0.17-0.56-0.28-1.14-0.33-1.72l-0.01-0.08c-0.04-0.46-0.04-0.92 0-1.38l0.06-0.97V70.9c0-0.71 0.08-1.41 0.23-2.1l0.23-0.68c0.09-0.24 0.23-0.45 0.41-0.62 0.17-0.16 0.38-0.28 0.6-0.34l0.18-0.05c0.37-0.11 0.76-0.1 1.12 0.03 0.14 0.05 0.28 0.11 0.4 0.2l1.04 0.69 2.21 1.62 2.67 1.86 1.94 1.17 1.77 0.95c0.5 0.27 1.04 0.5 1.58 0.7l1.97 0.73 0.12 0.02h0.1c0.11 0 0.21-0.1 0.23-0.21 0-0.1-0.04-0.19-0.12-0.23l-0.94-0.57-1.68-1.05-3.72-2.05-2.88-2.13-3.28-2.16-1.73-1.32c-1.35-1.18-2.58-2.49-3.68-3.9l-0.12-0.17-1.28-1.92-0.72-1.24-1.02-2.3c-0.52-1.4-0.9-2.86-1.1-4.34l-0.06-0.45-0.08-1.13-0.07-1.28 0.03-0.79c0-0.37 0.04-0.75 0.1-1.12l0.02-0.14 0.14-0.52c0.2-0.72 0.8-1.27 1.54-1.4l0.3-0.01c0.2-0.01 0.4 0 0.6 0.07 0.22 0.06 0.43 0.18 0.62 0.33l0.62 0.52L26.1 47c1.61 1.26 3.3 2.4 5.09 3.37 0.6 0.33 1.23 0.66 1.77 0.93 0.87 0.43 1.77 0.82 2.68 1.18l0.9 0.36 0.45 0.14c0.7 0.2 1.42 0.36 2.15 0.46l0.56 0.04h0.14c0.12 0 0.18-0.14 0.1-0.23l-0.04-0.02L39.5 53l-0.76-0.41-1.24-0.64-3.39-1.47-2.78-1.62-1.44-0.87-1.69-1.35-1.96-1.58-1.92-1.89-1.36-1.73-0.1-0.13c-0.88-1.16-1.64-2.42-2.27-3.75l-0.71-2.22-0.57-1.89c-0.12-0.57-0.2-1.16-0.22-1.75l-0.05-1.14L19 25.88l0.01-0.11c0.07-0.78 0.2-1.54 0.4-2.3l0.3-1.01 0.38-1.1 0.4-0.87c0.06-0.15 0.14-0.28 0.23-0.4l0.05-0.07c0.1-0.13 0.22-0.25 0.35-0.34 0.22-0.16 0.47-0.25 0.73-0.29h0.04c0.1-0.01 0.22-0.02 0.33-0.01l0.21 0.01c0.12 0.01 0.25 0.03 0.36 0.06h0.03c0.26 0.07 0.5 0.2 0.72 0.36 0.14 0.1 0.26 0.23 0.36 0.37l0.41 0.54 1.54 1.77 1.81 2.08c0.88 0.9 1.82 1.73 2.83 2.5l0.8 0.6 0.09 0.05c1.53 0.87 3.14 1.58 4.8 2.1"/>
|
||||||
|
</group>
|
||||||
|
<path
|
||||||
|
android:strokeColor="@color/popup_text"
|
||||||
|
android:strokeWidth="3.75"
|
||||||
|
android:pathData="M49.59 54.33v33.15 0.04c0 0.62 0.14 1.23 0.42 1.78m-0.42-34.97l-2.1-1.4-0.29-0.2c-1.67-1.17-3.26-2.46-4.75-3.86l-0.35-0.35c-1.54-1.53-2.88-3.24-4-5.1l-0.86-1.57c-0.45-0.82-0.82-1.68-1.1-2.57-0.46-1.43-0.7-2.92-0.7-4.42v-0.54-0.74c0-1.27 0.16-2.53 0.47-3.77 0.25-1 0.6-1.97 1.04-2.9l0.74-1.54 0.4-0.67c0.85-1.41 1.84-2.73 2.96-3.94l0.84-0.78c0.56-0.5 1.17-0.95 1.82-1.32l0.98-0.56 1.1-0.56 1.28-0.56 1-0.36c0.63-0.23 1.3-0.4 1.97-0.5 0.54-0.08 1.08-0.12 1.63-0.12h1.7 0.61c0.62 0 1.23 0.04 1.85 0.13 0.69 0.1 1.37 0.25 2.04 0.46l1.24 0.39 2.24 0.56 2.1 0.84 1.54 0.84 1.96 1.12 1.82 1.26 1.68 1.25 0.23 0.2c0.87 0.7 1.68 1.48 2.43 2.32l1.12 1.12 1.26 1.54 1.12 1.54 0.84 1.4 0.59 1.3M49.59 54.34l0.06 0.04c0.33 0.25 0.68 0.47 1.06 0.66l1.54 0.7 1.68 0.7 1.68 0.56 1.54 0.42 1.54 0.42 1.54 0.28 0.84 0.14 1.12 0.04 0.56-0.04h0.56m14.73-25.97l-1.01-0.05h-1.54-0.06c-0.8 0-1.58 0.14-2.32 0.42l-1.12 0.42-0.55 0.3c-0.66 0.35-1.28 0.78-1.86 1.26-0.54 0.45-1.04 0.94-1.49 1.48l-0.72 0.87-0.16 0.19c-0.83 1-1.57 2.05-2.22 3.17l-0.84 1.4-0.84 1.4-0.84 1.82-0.7 1.95-0.42 1.96-0.28 2.1v0.11c0 0.95 0.1 1.9 0.28 2.83l0.42 1.4c0.28 0.92 0.7 1.8 1.28 2.58l0.26 0.36m14.73-25.97l1.65 0.37c0.84 0.18 1.65 0.46 2.43 0.82l0.14 0.07c0.52 0.24 1.03 0.53 1.52 0.85l0.11 0.08c0.56 0.37 1.08 0.8 1.55 1.26 0.37 0.37 0.7 0.76 1 1.18l1.09 1.47c0.65 0.93 1.17 1.94 1.55 3l0.13 0.36 0.02 0.06c0.36 1.17 0.62 2.37 0.77 3.58v0.8c0 0.78-0.06 1.55-0.18 2.32-0.22 1.45-0.65 2.87-1.27 4.2l-0.13 0.27-0.89 1.64-1.54 2.1-0.42 0.5c-0.56 0.69-1.17 1.33-1.84 1.92l-0.06 0.05c-0.88 0.77-1.84 1.44-2.86 2l-0.25 0.11c-1.14 0.5-2.32 0.88-3.53 1.15l-0.17 0.04c-0.72 0.16-1.47 0.24-2.21 0.24h-0.79c-1.15 0-2.3-0.14-3.41-0.42l-1.82-0.7-1.68-0.7-0.62-0.33c-0.61-0.34-1.17-0.76-1.67-1.25-0.34-0.34-0.71-0.65-1.12-0.92l-0.23-0.15m0 0v1.81 0.84 1.4l-0.42 24.9v0.43c0 0.55-0.1 1.1-0.28 1.61M50 89.3l0.3 0.52c0.17 0.3 0.38 0.58 0.63 0.83 0.4 0.4 0.88 0.71 1.4 0.9l0.33 0.13 0.1 0.04c0.29 0.11 0.58 0.2 0.88 0.26 0.37 0.08 0.75 0.12 1.13 0.12h0.55 0.84H57h0.7c0.37 0 0.75-0.05 1.11-0.14l0.3-0.07c0.56-0.14 1.1-0.35 1.62-0.6l0.05-0.03 0.42-0.28 0.28-0.18c0.18-0.12 0.35-0.26 0.5-0.42 0.22-0.25 0.4-0.54 0.51-0.86l0.1-0.28M50 89.3l12.6-0.06M48.9 31.81l-0.86-0.65c-0.17-0.13-0.32-0.28-0.45-0.47-0.06-0.1-0.12-0.2-0.16-0.3l-0.14-0.32c-0.14-0.33-0.21-0.69-0.21-1.05 0-0.28 0.04-0.57 0.13-0.84L47.26 28l0.2-0.5 0.16-0.3c0.1-0.2 0.23-0.38 0.39-0.54 0.12-0.12 0.25-0.22 0.4-0.31l0.29-0.17c0.12-0.08 0.26-0.15 0.4-0.2l0.11-0.05c0.25-0.1 0.51-0.15 0.78-0.15 0.2 0 0.4 0.03 0.58 0.08l0.26 0.08c0.3 0.08 0.57 0.2 0.83 0.34l1.15 0.62 1.4 0.84 1.12 0.84 1.4 0.98 1.17 0.89 0.3 0.28c0.14 0.15 0.25 0.32 0.34 0.5l0.02 0.03c0.18 0.35 0.27 0.74 0.27 1.13v0.25 0.19c0 0.42-0.1 0.84-0.3 1.22-0.08 0.18-0.18 0.34-0.3 0.5l-0.1 0.12c-0.19 0.23-0.42 0.41-0.68 0.54-0.1 0.06-0.22 0.1-0.34 0.14l-0.21 0.06c-0.4 0.1-0.8 0.16-1.2 0.16h-0.37c-0.28 0-0.55-0.05-0.81-0.15l-0.12-0.05c-0.13-0.05-0.25-0.11-0.37-0.18l-1.5-0.88-1.82-1.25-1.82-1.26Zm36.96 17.06l0.06-0.42c0.05-0.37 0.05-0.74-0.01-1.11l-0.01-0.07c-0.03-0.14-0.06-0.29-0.11-0.43l-0.1-0.28c-0.07-0.23-0.2-0.43-0.37-0.6l-0.07-0.07c-0.15-0.15-0.34-0.26-0.55-0.31-0.16-0.04-0.32-0.05-0.48-0.02l-0.25 0.04c-0.23 0.04-0.46 0.1-0.67 0.22l-0.14 0.07c-0.25 0.12-0.49 0.28-0.7 0.46l-0.26 0.22c-0.27 0.23-0.51 0.48-0.74 0.75l-0.36 0.43-0.56 0.84-0.84 1.26-0.14 0.21c-0.28 0.42-0.51 0.87-0.7 1.33l-0.56 1.54-0.1 0.36c-0.12 0.4-0.18 0.84-0.18 1.27v0.39c0 0.24 0.04 0.47 0.11 0.7l0.08 0.22c0.06 0.18 0.16 0.35 0.3 0.49l0.05 0.05c0.1 0.1 0.23 0.18 0.37 0.23 0.14 0.04 0.28 0.06 0.43 0.04l0.36-0.06c0.26-0.03 0.52-0.11 0.76-0.23l0.44-0.22c0.4-0.2 0.77-0.45 1.11-0.74l0.47-0.4 0.03-0.04c0.73-0.81 1.37-1.69 1.93-2.62 0.37-0.65 0.69-1.33 0.95-2.04l0.17-0.48 0.28-0.98Z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
style="@style/Widget.ALN.AppWidget.Container"
|
style="@style/Widget.LibrePods.AppWidget.Container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
android:padding="0dp"
|
android:padding="0dp"
|
||||||
android:id="@+id/battery_widget"
|
android:id="@+id/battery_widget"
|
||||||
android:theme="@style/Theme.ALN.AppWidgetContainer"
|
android:theme="@style/Theme.LibrePods.AppWidgetContainer"
|
||||||
android:background="@drawable/widget_background">
|
android:background="@drawable/widget_background">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
style="@style/Widget.ALN.AppWidget.Container"
|
style="@style/Widget.LibrePods.AppWidget.Container"
|
||||||
android:id="@+id/noise_control_widget"
|
android:id="@+id/noise_control_widget"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:theme="@style/Theme.ALN.AppWidgetContainer">
|
android:theme="@style/Theme.LibrePods.AppWidgetContainer">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@android:id/background"
|
android:id="@android:id/background"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
|
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
|
||||||
-->
|
-->
|
||||||
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
<style name="Theme.LibrePods.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
<color name="popup_text">@color/white</color>
|
<color name="popup_text">@color/white</color>
|
||||||
<color name="widget_background">#1C1B1E</color>
|
<color name="widget_background">#1C1B1E</color>
|
||||||
<color name="widget_text">@color/white</color>
|
<color name="widget_text">@color/white</color>
|
||||||
|
<color name="colorBackground">#0B0B0B</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Widget.ALN.AppWidget.Container" parent="android:Widget">
|
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget">
|
||||||
<item name="android:id">@android:id/background</item>
|
<item name="android:id">@android:id/background</item>
|
||||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
<item name="android:background">@drawable/app_widget_background</item>
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget">
|
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget">
|
||||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
||||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Widget.ALN.AppWidget.Container" parent="android:Widget">
|
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget">
|
||||||
<item name="android:id">@android:id/background</item>
|
<item name="android:id">@android:id/background</item>
|
||||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
<item name="android:background">@drawable/app_widget_background</item>
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
<item name="android:clipToOutline">true</item>
|
<item name="android:clipToOutline">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget">
|
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget">
|
||||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
||||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
|
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
|
||||||
and @android:dimen/system_app_widget_internal_padding requires API level 31
|
and @android:dimen/system_app_widget_internal_padding requires API level 31
|
||||||
-->
|
-->
|
||||||
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
<style name="Theme.LibrePods.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,4 +10,5 @@
|
|||||||
<color name="light_blue_200">#FF81D4FA</color>
|
<color name="light_blue_200">#FF81D4FA</color>
|
||||||
<color name="light_blue_600">#FF039BE5</color>
|
<color name="light_blue_600">#FF039BE5</color>
|
||||||
<color name="light_blue_900">#FF01579B</color>
|
<color name="light_blue_900">#FF01579B</color>
|
||||||
|
<color name="colorBackground">#FFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">ALN</string>
|
<string name="app_name" translatable="false">LibrePods</string>
|
||||||
<string name="title_activity_custom_device" translatable="false">GATT Testing</string>
|
<string name="title_activity_custom_device" translatable="false">GATT Testing</string>
|
||||||
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
|
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
|
||||||
<string name="accessibility">Accessibility</string>
|
<string name="accessibility">Accessibility</string>
|
||||||
@@ -49,4 +49,14 @@
|
|||||||
<string name="island_moved_to_remote_text">Moved to Linux</string>
|
<string name="island_moved_to_remote_text">Moved to Linux</string>
|
||||||
<string name="head_tracking">Head Tracking</string>
|
<string name="head_tracking">Head Tracking</string>
|
||||||
<string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string>
|
<string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string>
|
||||||
|
<string name="general_settings_header">General</string>
|
||||||
|
<string name="qs_click_behavior_title">Quick Settings Tile Action</string>
|
||||||
|
<string name="qs_click_behavior_dialog_desc">Show noise control dialog on tap.</string>
|
||||||
|
<string name="qs_click_behavior_cycle_desc">Cycle through modes on tap.</string>
|
||||||
|
<string name="developer_options_header">Developer</string>
|
||||||
|
<string name="more_settings_title">Open AirPods Settings</string>
|
||||||
|
<string name="more_settings_subtitle">Manage AirPods features and preferences</string>
|
||||||
|
<string name="ear_detection">Automatic Ear Detection</string>
|
||||||
|
<string name="auto_play">Auto Play</string>
|
||||||
|
<string name="auto_pause">Auto Pause</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Widget.ALN.AppWidget.Container" parent="android:Widget">
|
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget">
|
||||||
<item name="android:id">@android:id/background</item>
|
<item name="android:id">@android:id/background</item>
|
||||||
<item name="android:background">?android:attr/colorBackground</item>
|
<item name="android:background">?android:attr/colorBackground</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget">
|
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget">
|
||||||
<item name="android:background">?android:attr/colorBackground</item>
|
<item name="android:background">?android:attr/colorBackground</item>
|
||||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<style name="Theme.ALN" parent="android:Theme.Material.Light.NoActionBar" />
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.LibrePods" parent="Theme.AppCompat.DayNight">
|
||||||
|
<!-- Customize your light theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||||
|
<item name="android:windowLightStatusBar" >true</item>
|
||||||
|
<item name="android:windowLightNavigationBar" >true</item>
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
<!-- Theme for the transparent dialog activity -->
|
||||||
|
<style name="Theme.TransparentDialog" parent="Theme.AppCompat.Dialog">
|
||||||
|
<item name="android:windowIsTranslucent">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowIsFloating">false</item> <!-- Set to false for full width -->
|
||||||
|
<item name="android:backgroundDimEnabled">true</item> <!-- Dim background -->
|
||||||
|
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item> <!-- Optional: Add animation -->
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
<!-- Request blur behind (Android 12+) -->
|
||||||
|
<item name="android:windowBlurBehindEnabled" tools:targetApi="s">true</item>
|
||||||
|
<item name="android:windowBlurBehindRadius" tools:targetApi="s">32dp</item> <!-- Optional: Adjust radius -->
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.LibrePods.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
||||||
<item name="appWidgetRadius">32dp</item>
|
<item name="appWidgetRadius">32dp</item>
|
||||||
<item name="appWidgetPadding">0dp</item>
|
<item name="appWidgetPadding">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.ALN.AppWidgetContainer" parent="Theme.ALN.AppWidgetContainerParent">
|
<style name="Theme.LibrePods.AppWidgetContainer" parent="Theme.LibrePods.AppWidgetContainerParent">
|
||||||
<item name="appWidgetPadding">0dp</item>
|
<item name="appWidgetPadding">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
me.kavishdevar.aln.utils.KotlinModule
|
me.kavishdevar.librepods.utils.KotlinModule
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
com.android.bluetooth
|
com.android.bluetooth
|
||||||
me.kavishdevar.aln
|
me.kavishdevar.librepods
|
||||||
|
android
|
||||||
|
com.android.systemui
|
||||||
|
com.android.settings
|
||||||
|
com.google.android.bluetooth
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ kotlin.code.style=official
|
|||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
|
||||||
|
android.javaCompile.suppressSourceTargetDeprecationWarning=true
|
||||||
@@ -1,43 +1,40 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanistPermissions = "0.36.0"
|
accompanistPermissions = "0.36.0"
|
||||||
agp = "8.8.2"
|
agp = "8.8.2"
|
||||||
hiddenapibypass = "4.3"
|
hiddenapibypass = "6.1"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.1.10"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.16.0"
|
||||||
junit = "4.13.2"
|
|
||||||
junitVersion = "1.2.1"
|
|
||||||
espressoCore = "3.6.1"
|
|
||||||
lifecycleRuntimeKtx = "2.8.7"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
activityCompose = "1.9.3"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2024.11.00"
|
composeBom = "2025.04.00"
|
||||||
annotations = "26.0.0"
|
annotations = "26.0.2"
|
||||||
navigationCompose = "2.8.4"
|
navigationCompose = "2.8.9"
|
||||||
constraintlayout = "2.2.0"
|
constraintlayout = "2.2.1"
|
||||||
haze = "1.1.1"
|
haze = "1.5.3"
|
||||||
hazeMaterials = "1.1.1"
|
hazeMaterials = "1.5.3"
|
||||||
|
sliceBuilders = "1.1.0-alpha02"
|
||||||
|
sliceCore = "1.1.0-alpha02"
|
||||||
|
sliceView = "1.1.0-alpha02"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
|
hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
|
||||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
|
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
|
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
|
||||||
haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "hazeMaterials" }
|
haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "hazeMaterials" }
|
||||||
|
androidx-slice-builders = { group = "androidx.slice", name = "slice-builders", version.ref = "sliceBuilders" }
|
||||||
|
androidx-slice-core = { group = "androidx.slice", name = "slice-core", version.ref = "sliceCore" }
|
||||||
|
androidx-slice-view = { group = "androidx.slice", name = "slice-view", version.ref = "sliceView" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "ALN"
|
rootProject.name = "LibrePods"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
BIN
imgs/banner.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
@@ -179,7 +179,7 @@ EOF
|
|||||||
ui_print "Created script for apex library handling."
|
ui_print "Created script for apex library handling."
|
||||||
ui_print "You can now restart your device and test aln!"
|
ui_print "You can now restart your device and test aln!"
|
||||||
ui_print "Note: If your Bluetooth doesn't work anymore after restarting, then uninstall this module and report the issue at the link below."
|
ui_print "Note: If your Bluetooth doesn't work anymore after restarting, then uninstall this module and report the issue at the link below."
|
||||||
ui_print "https://github.com/kavishdevar/aln/issues/new"
|
ui_print "https://github.com/kavishdevar/librepods/issues/new"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
ui_print "Error: patched file missing."
|
ui_print "Error: patched file missing."
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ version=v3
|
|||||||
versionCode=3
|
versionCode=3
|
||||||
author=@devnoname120 and @kavishdevar
|
author=@devnoname120 and @kavishdevar
|
||||||
description=Fixes the Bluetooth L2CAP connection issue with AirPods
|
description=Fixes the Bluetooth L2CAP connection issue with AirPods
|
||||||
updateJson=https://raw.githubusercontent.com/kavishdevar/aln/main/update.json
|
updateJson=https://raw.githubusercontent.com/kavishdevar/librepods/main/update.json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v0.0.3",
|
"version": "v0.0.3",
|
||||||
"versionCode": 3,
|
"versionCode": 3,
|
||||||
"zipUrl": "https://github.com/kavishdevar/aln/releases/download/v0.0.3/btl2capfix-v0.0.3.zip",
|
"zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.0.3/btl2capfix-v0.0.3.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/kavishdevar/aln/main/CHANGELOG.md"
|
"changelog": "https://raw.githubusercontent.com/kavishdevar/librepods/main/CHANGELOG.md"
|
||||||
}
|
}
|
||||||
|
|||||||