Compare commits

...
122 Commits
Author SHA1 Message Date
darken 65f9e1d9db Release: 2.17.6-rc0 2025-07-03 00:19:40 +02:00
darken c97c8f4852 Refactor: Introduce EdgeToEdgeHelper for consistent inset handling
This commit introduces an `EdgeToEdgeHelper` class to centralize and simplify the application of window insets for edge-to-edge display.

Key changes:
- Created `EdgeToEdgeHelper` to manage padding based on system bar insets.
- Migrated various Fragments (`OverviewFragment`, `OnboardingFragment`, `SettingsFragment`, `TroubleShooterFragment`) to use `EdgeToEdgeHelper` for consistent padding.
- Removed manual inset handling from `Activity2` and `Fragment2` as `EdgeToEdgeHelper` now manages this.
- Enabled edge-to-edge display in `MainActivity`.
- Minor updates to `BillingClientConnection` for product details fetching and `build.gradle.kts` for configuration.
2025-07-03 00:14:58 +02:00
darken 6b2073eff2 Update AGP, SDK, and BillingClient
This commit updates:
- Android Gradle Plugin to 8.11.0
- Compile/Target SDK to 36
- Google Play Billing Library to 8.0.0

The `BillingClient` is updated to use the new `PendingPurchasesParams` and `QueryPurchasesParams` APIs.
2025-07-03 00:14:58 +02:00
darken 2b08d9a4d0 Release: 2.17.5-rc0 2025-06-30 14:13:41 +02:00
darken 977f135c50 Allow empty keys to clear IRK/EncKey
This commit modifies the handling of Identity Resolving Key (IRK) and Encryption Key (EncKey) to allow users to clear these values by providing an empty input.

Specifically:
- In `GeneralSettingsFragment.kt`, the `onKey` callbacks for both IRK and EncKey dialogs now use `takeIf { it.isNotEmpty() }` after converting the input hex string to a byte array. This ensures that an empty input results in `null` being set for the respective key.
- In `PodMonitor.kt`, when determining the main device, `mainDeviceIdentityKey.value` is now checked with `takeIf { it.isNotEmpty() }` to ensure an empty IRK is treated as no IRK being configured.
- In `AppleFactory.kt`, when attempting to decrypt the private payload, `mainDeviceEncryptionKey.value` is now checked with `takeIf { it.isNotEmpty() }` to ensure an empty EncKey prevents decryption attempts.
2025-06-30 14:12:32 +02:00
darken 3c777d377e Only use IRK match for main device if IRK is configured
This commit modifies `PodMonitor.kt` to ensure that the `isIRKMatch` flag is only used to determine the main device if an Identity Resolving Key (IRK) is actually configured in the general settings.

If no IRK is set, the main device determination will fall back to other logic, preventing a device from being incorrectly selected as "main" solely based on an IRK match when no specific IRK is being looked for.
2025-06-30 14:12:32 +02:00
darken b98994f2f1 Revert "Fix IRK-only mode to prevent heuristic fallback + auto battery optimization"
This reverts commit ca5041b27d.
2025-06-30 14:12:32 +02:00
wnerhs 85860a53bf Fix IRK-only mode to prevent heuristic fallback + auto battery optimization
- Prevent heuristic device detection when IRK/ENC keys are configured
- Maintain last IRK-matched device state instead of fallback
- Auto-enable LOW_POWER scan mode when IRK device is found
- Fixes false positives in crowded places
2025-06-30 14:12:32 +02:00
darken 7a967ce648 Release: 2.17.4-rc0 2025-06-23 14:06:20 +02:00
darken 4c7eac870d Dependencies: Downgrade Navigation Core to 2.8.9
Navigation Core version 2.9.0 seems to have a bug causing an `ConcurrentModificationException`.

See:
https://issuetracker.google.com/issues/188860458
https://issuetracker.google.com/issues/417784831
2025-06-23 14:04:39 +02:00
darken ff58e01551 Shorten app title for Tamil, Romanian, and Swedish
This commit updates the app title in the Google Play Store metadata for Tamil (ta-IN), Romanian (ro), and Swedish (sv-SE) by removing or adjusting a word to meet length constraints.
2025-06-22 21:28:55 +02:00
darken 004ea6cbcb Add fastlane/README.md to .gitignore
This commit updates the `.gitignore` file to include `fastlane/README.md`, ensuring that this Fastlane-generated file is not tracked by Git.
2025-06-22 21:28:55 +02:00
darken a42b88aa78 Fix typo in Lao app title
This commit corrects a typographical error in the Lao translation of the app title.
Specifically, it changes "ຄູ່​ຮ່ວມ​ສຳ​ລັບ" to "ຄູ່ຮ່ວມສຳລັບ" in `fastlane/metadata/android/lo-LA/title.txt`.
2025-06-22 21:28:55 +02:00
darken 43088cf8b8 Update Fastlane and add Wear OS lanes
This commit updates various Ruby gems in `Gemfile.lock`, most notably upgrading `fastlane` from version 2.213.0 to 2.228.0.

It also introduces new Fastlane lanes for Wear OS builds:
- `beta_wearos`
- `production_wearos`

The `fastlane/README.md` has been updated to reflect these new lanes.
2025-06-22 21:28:55 +02:00
darken 9460584891 More language updates and mapping changes 2025-06-22 21:28:55 +02:00
darken 570118ca94 Refactor Crowdin language mapping
This commit refactors the `crowdin.yaml` configuration file to:
- Use YAML anchors (`&stringmapping`, `*stringmapping`) to define and reuse the `languages_mapping` for different source files. This reduces redundancy and improves maintainability.
- Update language codes for Sardinian ("sc") and Norwegian ("no") to "sc-rIT" and "nb" respectively in the `stringmapping`.
- Update language codes for Sardinian ("sc") and Albanian ("sq") to "sc-IT" and "sq-AL" respectively in the `playstoremapping`.
2025-06-22 19:20:47 +02:00
darken d9f0de2e05 Add translations for Filipino, Albanian, Sardinian, and Urdu
This commit introduces new translations and updates existing ones for the CAPod application in Filipino (fil), Albanian (sq-rAL), Sardinian (sc-rIT), and Urdu (ur-rIN).

Specifically, it:
- Adds comprehensive translations for various UI elements, settings, permissions, and status messages in Albanian (`app/src/main/res/values-sq-rAL/strings.xml` and `app-common/src/main/res/values-sq-rAL/strings.xml`).
- Adds translations for Google Play related strings in Sardinian (`app/src/gplay/res/values-sc-rIT/strings.xml`).
- Adds translations for FOSS-specific upgrade options in Sardinian (`app/src/foss/res/values-sc-rIT/strings.xml`).
- Adds comprehensive translations for various UI elements, settings, permissions, and status messages in Sardinian (`app/src/main/res/values-sc-rIT/strings.xml`).
- Updates and expands translations in Filipino (`app-common/src/main/res/values-fil/strings.xml`), including terms for app names, general UI elements, permissions, device states, and settings.
- Adds comprehensive translations for various UI elements, settings, permissions, and status messages in Urdu (`app-common/src/main/res/values-ur-rIN/strings.xml`).
2025-06-22 19:20:47 +02:00
darken 3d5dcfb21d Add Sardinian and Albanian to Crowdin config
This commit updates the `crowdin.yaml` file to include language mappings for Sardinian (sc-IT) and Albanian (sq-AL).

Specifically, it adds:
- `sc-IT: sc-rIT` and `sq-AL: sq-rAL` to the `android_code` language mappings for various files.
- `sc-IT: sc-IT` and `sq-AL: sq-AL` to the `locale` language mappings under `playstoremapping`.
2025-06-22 19:20:47 +02:00
darken e0dc9ea737 Fix escaped quotes in translations
This commit corrects the escaping of quotation marks in several translated strings. Specifically, it replaces `\\\"` with `\"` in:
- Galician (gl-rES) translations for various permission descriptions.
- Telugu (te-rIN) translations for settings descriptions and troubleshooter text.
- Romansh (rm) translations for settings descriptions and troubleshooter text.
2025-06-22 19:20:47 +02:00
darken b4d0904f55 Add translations for Zulu, Finnish, Galician, Kyrgyz, Romansh, Telugu, and Uzbek
This commit introduces new translations for the app in Zulu (zu), Finnish (fi), Galician (gl-rES), Kyrgyz (ky-rKG), Romansh (rm), Telugu (te-rIN), and Uzbek (uz).
2025-06-22 19:20:47 +02:00
darken 2caee59713 More translation updates 2025-06-22 19:20:47 +02:00
darken 6712ba78ba Update app translations 2025-06-22 19:20:47 +02:00
darken cba30aee47 Translate app store listings to various languages
This commit translates the app's title, short description, and full description into several new languages for the app store listing.

Specifically, it adds translations for:
- Tamil (ta-IN)
- Kurdish (kmr-TR)
- Armenian (hy-AM)
- Kyrgyz (ky-KG)
- Swedish (sv-SE)
- Uzbek (uz)
- Telugu (te-IN)
- Lao (lo-LA)
- Icelandic (is-IS)
- Slovenian (sl)
- Macedonian (mk-MK)
- Afrikaans (af)
- Serbian (sr)
- Zulu (zu)
- Croatian (hr)
- Basque (eu-ES)
- Amharic (am)
- Urdu (ur-IN)
- Mongolian (mn-MN)
- Latvian (lv)
- Malayalam (ml-IN)
- Filipino (fil)
- Marathi (mr-IN)
- Finnish (fi-FI)
- Swahili (sw)
- Georgian (ka-GE)
- Thai (th)
- Burmese (my-MM)
- Romansh (rm)
- Sardinian (sc-IT)
- Bengali (bn-BD)
- Nepali (ne-NP)
- Bulgarian (bg)
- Albanian (sq-AL)
- Sinhala (si-LK)
- Khmer (km-KH)
- Galician (gl-ES)
- Lithuanian (lt)
- Danish (da-DK)
- Kannada (kn-IN)
2025-06-22 19:20:47 +02:00
darken c6cd8d68f7 Upgrade Android Gradle Plugin and KSP
This commit updates the Android Gradle Plugin (AGP) from version 8.10.0 to 8.10.1 and the Kotlin Symbol Processing (KSP) plugin from version 2.1.20-2.0.0 to 2.1.21-2.0.1.

These changes are applied in the root `build.gradle.kts` file and the `buildSrc/build.gradle.kts` file.
2025-06-22 18:08:22 +02:00
darken d2fbf9d79f Add variant PowerBeatsPro test case 2025-06-21 14:01:16 +02:00
darken 738a35dbe9 Add support for PowerBeats Pro 2
This commit introduces support for the PowerBeats Pro 2 device.

Had to change the PowerBeats Pro 1 device code due to conflicting matches. Previously was using the half code variant. Might have broken that, but don't have any feedback from actual users, someone will have to speak up...

Closes #298
2025-06-21 14:01:16 +02:00
darken 1278ec55c2 Refine changelog translations 2025-06-20 11:00:23 +02:00
darken 95ff34b373 Add Fastlane changelogs for multiple languages
This commit adds default changelog files for various languages in the Fastlane metadata directory. Each file contains a generic message about bug fixes, performance improvements, and potential new features, along with a link to the full changelog and a note about the developer being a single person.
2025-06-20 10:21:15 +02:00
darken 1329429130 Refactor: Improve CHANGELOG.md formatting
This commit refactors the Liquid templating in `CHANGELOG.md` to:
- Indent Liquid logic for better readability.
- Improve spacing around headings (##, ###) and list items (-) for a cleaner rendered output.
2025-06-20 09:51:17 +02:00
darken 8c3c09dc03 feat: Enhance Jekyll config and add auto-generated changelog page
This commit updates the Jekyll configuration to include `jekyll-github-metadata` and `jemoji` plugins. It also introduces a `CHANGELOG.md` file that automatically generates a formatted changelog page from GitHub releases. The default changelog text for new app versions in Fastlane has also been updated to be more engaging and directly link to the new changelog page.
2025-06-20 08:58:39 +02:00
darken 10bf65bd09 Release: 2.17.3-rc0 2025-06-18 13:17:56 +02:00
darken b57b3458ab Update translations 2025-06-18 13:17:20 +02:00
darken 6aeee4816c Update translations 2025-06-18 13:17:20 +02:00
darken 3c8f24fa50 Release: 2.17.2-rc0 2025-06-13 10:04:07 +02:00
darken 5e60629b4c Update translations 2025-06-13 10:02:59 +02:00
darken 21d740ae51 Fix IRK match flag in AppleFactory
This commit corrects the `isIRKMatch` flag assignment in `AppleFactory.kt`. Previously, it was hardcoded to `true`. Now, it correctly uses the `isIrkMatch` variable determined by the Identity Resolving Key (IRK) comparison.
2025-06-13 09:27:43 +02:00
darken f4e8f08f4f Release: 2.17.1-rc1 2025-06-12 09:02:30 +02:00
darken 7bf472023f Update translations 2025-06-12 08:57:16 +02:00
darken 01fc9654f8 Refine default changelog text 2025-06-12 08:57:16 +02:00
darken c1b247c219 Release: 2.17.0-rc0 2025-06-06 07:47:56 +02:00
darken 101fb68205 Translate strings for new notification channel and update descriptions
This commit introduces translations for a new notification channel "Device connected" and updates some existing string descriptions in various languages.

Specifically, it:
- Adds translations for `settings_monitor_connected_notification_label`, `settings_monitor_connected_notification_description`, and `notification_channel_device_status_connected_label` in Spanish (es), French (fr), Estonian (et-rEE), Norwegian (no), Czech (cs), Polish (pl), Catalan (ca), Greek (el), and German (de).
- Updates the full app descriptions in `fastlane/metadata/android/` for Catalan (ca), German (de-DE), Spanish (es-ES), French (fr-FR), Estonian (et), Norwegian (no-NO), Czech (cs-CZ), Polish (pl-PL), and Russian (ru-RU) to replace "phone" with "device" in the context of connecting to AirPods.
- Adds missing Arabic (ar) translations for various strings related to debug logging, settings, troubleshooting, and onboarding.
- Fixes a typo in the German translation for `settings_general_description`.
- Corrects Hebrew (iw) translations for Bluetooth permission descriptions.
2025-06-06 06:57:29 +02:00
darken cda16ca06a Extend tests and fix payload parsing
This commit enhances the test coverage for `AirPodsProTest` by:
- Verifying the hex representation of the public payload data.
- Confirming that the private payload is null when no IRK/EncKey is set.
- Verifying the hex representation of the private payload data when IRK/EncKey are set.

Additionally, a fix is introduced in `AppleFactory.kt` to correctly parse the public payload by taking only the first 9 bytes instead of 16. This ensures that the private payload is only processed if the incoming message length matches the expected `PAIRING_MESSAGE_LENGTH`.
2025-06-06 06:03:41 +02:00
darken e9b29a2a43 Prioritize private payload for battery info in single headset devices
This commit modifies `SingleApplePods`, `AirPodsMaxUsbc`, and `AirPodsMax` to prioritize battery information from the decrypted private payload when available.

Specifically:
- `SingleApplePods.batteryHeadsetPercent`: Now checks `payload.private?.asBatteryState(1)?.level` first. If the private payload is not available or doesn't contain battery information at position 1, it falls back to the existing logic using `pubPodsBattery`.
- `AirPodsMaxUsbc.isHeadsetBeingCharged` and `AirPodsMax.isHeadsetBeingCharged`: Now check `payload.private?.asBatteryState(1)?.isCharging` first. If the private payload is not available or doesn't contain charging status at position 1, they fall back to the existing logic using `pubFlags.isBitSet(0)`.

This change allows for more accurate battery level and charging status reporting when the private payload can be decrypted.
2025-06-06 06:03:41 +02:00
darken 5df809a492 Improve main device determination logic
This commit refines the logic for selecting the main Pod device. Previously, the `RPAChecker` was used directly to verify the Identity Resolving Key (IRK).

The new approach leverages the `isIRKMatch` flag within the `ApplePods` class. This flag is set during the device parsing process, indicating whether the device's address matches the configured IRK.

This change:
- Simplifies the `PodMonitor` by removing the direct dependency on `RPAChecker`.
- Encapsulates the IRK matching logic within the `ApplePods` class, improving modularity.
- Ensures that the main device selection correctly prioritizes devices whose IRK has been matched.
2025-06-06 06:03:41 +02:00
darken ffe8a58746 Introduce ApplePods.Flags and UI for key status
This commit introduces a new `Flags` data class within `ApplePods` to store boolean flags related to the device, specifically `isIRKMatch`.

This flag is now passed during the creation of `ApplePods` instances in all `ApplePodsFactory` implementations and the central `AppleFactory`.

The UI has been updated to display a key icon in the overview cards (`DualPodsCardVH` and `SinglePodsCardVH`):
- The icon is only visible if `flags.isIRKMatch` is true.
- The icon changes to a filled key (`ic_key_24`) if `payload.private` is not null (indicating successful decryption), otherwise an outline key (`ic_key_outline_24`) is shown.
- Minor layout adjustments were made in `overview_pods_single_item.xml` and `overview_pods_dual_item.xml` to accommodate the new key icon.
2025-06-06 06:03:41 +02:00
darken eab0296125 Introduce helper for parsing battery state
This commit introduces a new `BatteryState` data class and an extension function `ProximityPayload.Private.asBatteryState` to simplify the parsing of battery level and charging status from the proximity payload.

The `asBatteryState` function takes the position of the battery data in the payload and returns a `BatteryState` object, or null if the data is invalid.

This change improves code readability and maintainability by encapsulating the battery state parsing logic.
2025-06-06 06:03:41 +02:00
darken 2ff143b858 Determine case charging state more accurately
The previous logic for determining if the case was charging relied on the `batteryCasePercent` being non-null. This commit refines the logic to check if the `batteryCasePercent` falls within the valid range of 0.0 to 1.0, providing a more accurate assessment of the case's charging status.
2025-06-06 06:03:41 +02:00
darken 3b2dcc1a5c Refactor Pods history to a central PodHistoryRepo
This moves the history tracking logic for discovered Apple Pods from individual `ApplePodsFactory` implementations to a new `PodHistoryRepo` class.

Benefits:
- Centralized logic for device history management.
- Simplifies individual `ApplePodsFactory` implementations.
- Improves code maintainability and testability.
2025-06-06 06:03:41 +02:00
darken d59dc8c225 Move RPA checker and key types
Relocate RPAChecker to the `pods.core.apple.protocol` package as it's specific to Apple's protocol.

Introduce type aliases `IdentityResolvingKey` and `ProximityEncryptionKey` for `ByteArray` to improve code clarity and type safety when dealing with these keys. Update relevant classes to use these new type aliases.
2025-06-06 06:03:41 +02:00
darken 877c5d9013 Introduce BluetoothAddress typealias
This commit introduces a typealias `BluetoothAddress` for `String` to improve code clarity and type safety when dealing with Bluetooth MAC addresses.

The following changes were made:
- Created `BluetoothAddress.kt` defining the typealias.
- Updated various classes to use `BluetoothAddress` instead of `String` for Bluetooth addresses:
    - `RPAChecker`
    - `PopUpReaction`
    - `DeviceSelectionDialogFactory`
    - `MonitorWorker`
    - `GeneralSettings`
    - `BluetoothDevice2`
2025-06-06 06:03:41 +02:00
darken 11a19da0b8 Add helper for HEX string to byte array conversion
This commit introduces a new helper function `fromHex()` that converts a HEX string (with optional separators like spaces or hyphens) into a byte array. It also adds a corresponding `toHex()` function for converting a byte array to a HEX string.

These helpers are now used in various parts of the codebase, including:
- Fake BLE data generation
- AirPod key input dialog
- RPA checker tests
- BaseAirPodsTest

Additionally, a unit test for these conversion functions has been added.
2025-06-06 06:03:41 +02:00
darken eda81822aa Add more tests for AirPods data decryption
This commit adds a test case to `AirPodsProTest` to verify the decryption of AirPods Pro data with and without encryption/IRK keys. It also introduces a new test class `RPACheckerTest` to validate the RPA checking mechanism.

Helper functions for converting hex strings to byte arrays and setting mock keys have been added to `BaseAirPodsTest` to simplify test setup.
2025-06-06 06:03:41 +02:00
darken 3b00613dde Refactor proximity message handling, introduce "Payload" that splits the data in public and private part. 2025-06-06 06:03:41 +02:00
darken a434a0bee0 Disable encryption key setting if no identity key
The encryption key setting for the main device is now disabled if the identity key is not set. This prevents users from trying to set an encryption key for a device that doesn't have an identity key.
2025-06-06 06:03:41 +02:00
darken 33ca43eaf1 Use decrypted proximity data if available
This change introduces the capability to decrypt the last 16 bytes of the proximity pairing message if an encryption key is available.

The decrypted payload provides more precise battery information (0-100% instead of 0-10%) and charging status for each pod and the case.

This information is now used by `DualApplePods` to enhance battery reporting accuracy.

The `RPAChecker` has also been updated with improved error logging.
2025-06-06 06:03:41 +02:00
darken df4ec3501b Add resolvable private address check
This allows verifying the identity of a device if its Identity Resolving Key (IRK) is known.
The IRK can be optionally configured.
2025-06-06 06:03:41 +02:00
darken fc54e540e8 Code fluff
Replace `values()` with `entries` for enum iteration, as recommended by Kotlin for improved performance and consistency.

Also:
- Suppress unused warning in `ByteArrayAdapter`
- Improve logging in `WebpageTool` by including the exception details.
2025-06-06 06:03:41 +02:00
darken e8481cc8cd Add preferences for AirPods Identity and Encryption Keys
This commit introduces settings for users to input their AirPods' Identity Resolving Key (IRK) and Encryption Key. These keys, obtainable via a MacBook, allow CAPod to reliably identify the AirPods and decrypt detailed status information.

Key changes:
- Added new preferences in General Settings for IRK and Encryption Key.
- Implemented a custom dialog for key input, including validation and a link to a guide.
- Added a Moshi adapter for serializing/deserializing `ByteArray` to/from Base64 for storing the keys.
- Included new string resources for labels, descriptions, and explanations related to the keys.
- Added new icons for the key preferences.
2025-06-06 06:03:41 +02:00
darken eb8a9b3b46 Add option for separate connected notification
Closes #28
Closes #18
2025-05-14 21:21:04 +02:00
darken 79ec3ba932 Remove obsolete configuration in manifest 2025-05-14 21:04:28 +02:00
darken 8f910b6dcc Group reaction preferences into categories 2025-05-14 19:35:54 +02:00
darken 92fa181925 Decrease default minimum signal quality 2025-05-14 19:35:47 +02:00
darken ca82f96b30 Update translations 2025-05-13 19:54:01 +02:00
darken 2ea2bb1765 Update Play Billing library to 7.1.1 2025-05-13 19:02:02 +02:00
darken fb22798469 Use correct log levels for workmanager 2025-05-13 19:02:02 +02:00
darken d48f475a34 Update desugar_jdk_libs to 2.1.5 2025-05-13 19:02:02 +02:00
darken c88c57e856 Update gradle dependencies and configurations 2025-05-13 19:02:02 +02:00
darken 17892fbd24 Fix test runner 2025-04-11 03:02:23 +02:00
darken 2352974d4d Update github actions (based on SDMSE) and use pinning 2025-04-11 03:02:23 +02:00
darken e216fbf66e Update privacy policy link 2025-03-21 19:36:03 +01:00
Matthias Urhahn f3a947a8a3 Update PRIVACY_POLICY.md 2025-03-21 19:36:03 +01:00
darken 00264c1d2b Remove link to Twitter 2025-03-21 19:24:07 +01:00
darken 54a02837fd Update description with feedback from Crowdin 2025-03-21 19:23:06 +01:00
Matthias Urhahn 7a5af76ebb Create CNAME 2025-03-19 22:30:14 +01:00
Matthias Urhahn 92bd754623 Create plain.html 2025-03-19 22:29:09 +01:00
Matthias Urhahn 85b9978e05 Create _config.yml 2025-03-19 22:28:41 +01:00
darken 1ebb2319d4 Add thanks note 2024-12-05 13:43:06 +01:00
darken f956625d2e Update translations 2024-12-05 13:43:06 +01:00
darken 9c2ebd9d04 Update CLI tool 2024-12-05 13:43:06 +01:00
darken fb0527b0bf Release: 2.16.0-rc0 2024-11-08 11:18:14 +01:00
darken 6b922e8845 Fix AirPods Max and AirPods Max (USB-C) support
Closes #236
2024-11-08 09:24:44 +01:00
b4shtirk1n 663e2d3b08 add support airpods max 2 & fix airpods max 2024-11-08 08:23:46 +01:00
darken 858b86af6a Fix onboarding not being completable with large fontsizes 2024-09-30 16:17:11 +02:00
darken 647300fd3a Release: 2.15.0-rc0 2024-09-23 15:05:48 +02:00
darken 3eaf99390e Record debug logs without foreground service
Some overhaul of log recording to get rid of the `FOREGROUND_SERVICE_SPECIAL_USE` which is a headache with Google
2024-09-23 15:05:11 +02:00
darken 0ee7915e31 Fix WorkManager init crash
```
Caused by java.lang.IllegalStateException: WorkManager is not initialized properly.  You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
  at androidx.work.impl.WorkManagerImpl.getInstance (WorkManagerImpl.java)
  at androidx.work.impl.foreground.SystemForegroundDispatcher.<init> (SystemForegroundDispatcher.java)
  at androidx.work.impl.foreground.SystemForegroundService.initializeDispatcher (SystemForegroundService.java)
  at androidx.work.impl.foreground.SystemForegroundService.onCreate (SystemForegroundService.java)
  at android.app.ActivityThread.handleCreateService (ActivityThread.java:4726)
```
2024-09-23 14:12:25 +02:00
darken 10c880a800 Do staged rollouts via fastlane 2024-09-23 13:53:18 +02:00
Matthias Urhahn b1bf658ff3 Update README.md 2024-09-23 13:51:09 +02:00
Matthias Urhahn 77dc13d9ba Merge pull request #228 from d4rken-org/airpods-gen4-ANC
Add support for AirPods (Gen 4 with ANC)
2024-09-23 13:41:21 +02:00
darken 21b6cfc013 Add support for AirPods (Gen 4 with ANC)
Closes #226
2024-09-23 13:33:12 +02:00
Matthias Urhahn 5d953ee23e Merge pull request #227 from d4rken-org/airpods-gen4
Add support for AirPods (Gen 4 - no ANC)
2024-09-23 13:32:30 +02:00
darken 42f2f5bfee Add support for AirPods (Gen 4)
Closes #225
2024-09-23 13:08:35 +02:00
darken 6708901394 Improve BLE scanning
The default "balanced" mode was reporting data too infrequently.
2024-07-26 08:48:21 +02:00
darken 788038beb7 Release: 2.14.1-rc0 2024-07-12 21:47:39 +02:00
darken 18ac05ed80 Fix crashes when entering settings
Likely from #209
2024-07-12 21:47:04 +02:00
darken 02f68821d2 Release: 2.14.0-rc0 2024-07-12 15:04:28 +02:00
darken 62886f00f1 Show battery values in notification content titles 2024-07-12 14:47:01 +02:00
darken f2eb45d00b Fix app crash on launch if permission are not granted
```java
12:49:29.433 AndroidRuntime                       E  FATAL EXCEPTION: main
                                                     Process: eu.darken.capod, PID: 15164
                                                     java.lang.SecurityException: Starting FGS with type connectedDevice callerApp=ProcessRecord{cb03698 15164:eu.darken.capod/u0a352} targetSDK=34 requires permissions: all of the permissions allOf=true [android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE] any of the permissions allOf=false [android.permission.BLUETOOTH_ADVERTISE, android.permission.BLUETOOTH_CONNECT, android.permission.BLUETOOTH_SCAN, android.permission.CHANGE_NETWORK_STATE, android.permission.CHANGE_WIFI_STATE, android.permission.CHANGE_WIFI_MULTICAST_STATE, android.permission.NFC, android.permission.TRANSMIT_IR, android.permission.UWB_RANGING, USB Device, USB Accessory]
                                                     	at android.os.Parcel.createExceptionOrNull(Parcel.java:3242)
                                                     	at android.os.Parcel.createException(Parcel.java:3226)
                                                     	at android.os.Parcel.readException(Parcel.java:3209)
                                                     	at android.os.Parcel.readException(Parcel.java:3151)
                                                     	at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:7167)
                                                     	at android.app.Service.startForeground(Service.java:863)
                                                     	at androidx.work.impl.foreground.SystemForegroundService$Api31Impl.startForeground(SystemForegroundService.java:194)
                                                     	at androidx.work.impl.foreground.SystemForegroundService$1.run(SystemForegroundService.java:130)
                                                     	at android.os.Handler.handleCallback(Handler.java:959)
                                                     	at android.os.Handler.dispatchMessage(Handler.java:100)
                                                     	at android.os.Looper.loopOnce(Looper.java:232)
                                                     	at android.os.Looper.loop(Looper.java:317)
                                                     	at android.app.ActivityThread.main(ActivityThread.java:8699)
                                                     	at java.lang.reflect.Method.invoke(Native Method)
                                                     	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
                                                     	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
2024-07-12 13:44:57 +02:00
darken 2fca9097c2 Fix up-navigation behavior when doing onboarding
Back button shouldn't take you dashboard -> onboarding or onboarding -> dashboard
2024-07-12 13:44:44 +02:00
Ricky Cheung 051677e7dd Prevent return@apply duplicates
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung fbcd595b45 Add more spacing around the wide variant of dual pods widgets
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung aa38f58f62 setCustomContentView() -> setCustomBigContentView()
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung a3b01a02ee Update all checkbox preferences to SUW switches
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung 8544983984 Update com.google.android.material to 1.12.0
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung 07a11799b1 Implement support for different states in notification text
Fixes #198

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-27 08:34:19 +02:00
Kavish Devar 65a7b1933a Add PUSH_NOTIFICATIONS permission to AndroidManifest.xml for wearos (#203)
* Add PUSH_NOTIFICATIONS permissions to AndroidManifest.xml for wearos

Fixes https://github.com/d4rken-org/capod/issues/154

* Fix typo in AndroidManifest.xml for wearos app
2024-06-26 12:41:47 +02:00
Ricky Cheung bbfb735124 Enlarge the icons on wide widgets
* Also move the case details to the centre and separate the text a little

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 7b474fb0e1 Fix up comment
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung c6524d7cee Log layout ID in createDualPodLayout
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 148cdf5109 Add more notes for the wide widgets
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 60a3039415 Annotate layout argument as LayoutRes
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 6f7b6e30c9 Fix status bar icons in light theme
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 08c095eae4 Switch all MaterialComponents for Material3 for Wear
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 9087be6282 Add wide variant of dual pod widget
* Wide variant activates when the widget is larger than 3 columns in length

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung d46fe35eab Make widgets consistent with each other
Going by 2f9185efa7, it appears the label is preferred to be at the bottom

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 1afb77ee33 Make the status bar transparent
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung a9215fc11c Switch to everything Material3 components
This commit modifies all major elements of the UI for phones to use Material3 elements, including the toolbar.

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
darken d80a110280 Thank the MagicPods project 2024-04-30 16:10:06 +02:00
darken fdfc7f44fb Update translations 2024-04-30 15:37:18 +02:00
darken 26fbfc08f7 Remove proprietary Google Play info block in FOSS builds 2024-04-05 18:42:11 +02:00
hwlnx 89049ee3ab Update preferences_support.xml 2024-03-11 13:30:22 +01:00
hwlnx 68f81f900b Update strings.xml 2024-03-11 13:29:19 +01:00
700 changed files with 15996 additions and 3936 deletions
+42
View File
@@ -0,0 +1,42 @@
name: 'Common project setup'
description: 'Setup the base project'
runs:
using: "composite"
steps:
- name: Set up JDK 17
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 #v4.7.0
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
shell: bash
run: chmod +x gradlew
- name: Cache Gradle Wrapper
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
with:
path: |
~/.gradle/wrapper
!~/.gradle/wrapper/dists/**/gradle*.zip
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-wrapper-
- name: Cache Gradle Dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
with:
path: |
~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'buildSrc/**/*.kt') }}
restore-keys: |
${{ runner.os }}-gradle-caches-
- name: Cache Android Global Build-Cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
with:
path: |
~/.android/build-cache
key: ${{ runner.os }}-android-build-cache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-android-build-cache-
+14
View File
@@ -0,0 +1,14 @@
changelog:
exclude:
labels:
- changelog-ignore
categories:
- title: ":rocket: Enhancements"
labels:
- enhancement
- title: ":lady_beetle: Bug fixes"
labels:
- bug
- title: ":shrug: Other changes"
labels:
- "*"
+31 -23
View File
@@ -7,6 +7,24 @@ on:
branches: [ main ]
jobs:
lint-vital:
name: Lint vitals
strategy:
fail-fast: false
matrix:
flavor: [ Foss,Gplay ]
variant: [ Beta,Release ]
module: [ app ]
runs-on: ubuntu-22.04
steps:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup project and build environment
uses: ./.github/actions/common-setup
- name: Check for fatal lint issues
run: ./gradlew lintVital${{ matrix.flavor }}${{ matrix.variant }}
build-modules:
name: Build apps
strategy:
@@ -14,20 +32,15 @@ jobs:
matrix:
flavor: [ Foss,Gplay ]
variant: [ Debug ]
module: [ app,app-wear ]
runs-on: ubuntu-latest
module: [ app ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup project and build environment
uses: ./.github/actions/common-setup
- name: Build module
- name: Build modules
run: ./gradlew ${{ matrix.module }}:assemble${{ matrix.flavor }}${{ matrix.variant }}
test-modules:
@@ -35,19 +48,14 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [ Release ]
variant: [ Debug,Beta,Release ]
flavor: [ testFoss,testGplay ]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup project and build environment
uses: ./.github/actions/common-setup
- name: Test modules
run: ./gradlew ${{ matrix.flavor }}${{ matrix.variant }}UnitTest
@@ -11,7 +11,7 @@ on:
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 #v4.3.1
+21 -27
View File
@@ -8,7 +8,9 @@ on:
jobs:
release-github:
name: Create GitHub release
runs-on: ubuntu-latest
permissions:
contents: write
runs-on: ubuntu-22.04
environment: foss-production
steps:
- name: Decode Keystore
@@ -21,25 +23,22 @@ jobs:
echo $ENCODED_KEYSTORE | base64 -di > "${TMP_KEYSTORE_FILE_PATH}"
echo "STORE_PATH=$(echo $TMP_KEYSTORE_FILE_PATH)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
fetch-depth: 0
- name: Setup project and build environment
uses: ./.github/actions/common-setup
- name: Get the version
id: tagger
uses: jimschubert/query-tag-action@v2
uses: jimschubert/query-tag-action@0b288a5fff630fea2e96d61b99047ed823ca19dc #v2.2
with:
skip-unshallow: 'true'
abbrev: false
commit-ish: HEAD
- name: Install JDK ${{ matrix.java_version }}
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 17
- name: Assemble beta APK
if: contains(steps.tagger.outputs.tag, '-beta')
run: ./gradlew assembleFossBeta
@@ -60,7 +59,7 @@ jobs:
- name: Create pre-release
if: contains(steps.tagger.outputs.tag, '-beta')
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda #v2.2.1
with:
prerelease: true
tag_name: ${{ steps.tagger.outputs.tag }}
@@ -74,21 +73,19 @@ jobs:
- name: Create release
if: "!contains(steps.tagger.outputs.tag, '-beta')"
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda #v2.2.1
with:
prerelease: false
tag_name: ${{ steps.tagger.outputs.tag }}
name: ${{ steps.tagger.outputs.tag }}
generate_release_notes: true
files: |
app/build/outputs/apk/foss/release/*.apk
app-wear/build/outputs/apk/foss/release/*.apk
files: app/build/outputs/apk/foss/release/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-gplay:
name: Create Google Play release
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
environment: gplay-production
steps:
- name: Decode Keystore
@@ -111,29 +108,26 @@ jobs:
echo $ENCODED_SERVICE_KEY | base64 -di > "${TMP_SERVICEKEY_FILE_PATH}"
echo "SUPPLY_JSON_KEY=$(echo $TMP_SERVICEKEY_FILE_PATH)" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
fetch-depth: 0
- name: Setup project and build environment
uses: ./.github/actions/common-setup
- name: Get the version
id: tagger
uses: jimschubert/query-tag-action@v2
uses: jimschubert/query-tag-action@0b288a5fff630fea2e96d61b99047ed823ca19dc #v2.2
with:
skip-unshallow: 'true'
abbrev: false
commit-ish: HEAD
- name: Install JDK ${{ matrix.java_version }}
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 17
- name: Set up ruby env
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 #v1.229.0
with:
ruby-version: 2.7.6
ruby-version: 3.3.6
bundler-cache: true
# - name: Assemble WearOS beta and upload to Google Play
+2 -1
View File
@@ -12,4 +12,5 @@
*.jks
.local/*
/fastlane/report.xml
/fastlane/Appfile
/fastlane/Appfile
/fastlane/README.md
+1
View File
@@ -9,6 +9,7 @@
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
+90
View File
@@ -0,0 +1,90 @@
---
layout: plain
permalink: /changelog
title: "Changelog"
---
# Changelog for CAPod
{% for release in site.github.releases %}
## {{ release.tag_name }} - {{ release.published_at | date: "%B %d, %Y" }}
{% assign clean_body = release.body | strip %}
{% assign no_comments = clean_body | replace: "<!-- Release notes generated using configuration in .github/release.yml", "" %}
{% assign no_comments = no_comments | split: "-->" %}
{% if no_comments.size > 1 %}
{% assign clean_content = no_comments[1] | strip %}
{% else %}
{% assign clean_content = no_comments[0] | strip %}
{% endif %}
{% comment %} Make links clickable {% endcomment %}
{% assign lines = clean_content | split: "
" %}
{% assign processed_lines = "" %}
{% for line in lines %}
{% if line contains "**Full Changelog**:" %}
{% comment %} Handle Full Changelog links {% endcomment %}
{% assign parts = line | split: ": " %}
{% if parts.size > 1 %}
{% assign url = parts[1] | strip %}
{% assign clickable_line = "**[View Changes](" | append: url | append: ")**" %}
{% assign processed_lines = processed_lines | append: clickable_line | append: "
" %}
{% else %}
{% assign processed_lines = processed_lines | append: line | append: "
" %}
{% endif %}
{% elsif line contains " in https://github.com/" and line contains "/pull/" %}
{% comment %} Handle pull request links {% endcomment %}
{% assign pr_parts = line | split: " in https://github.com/" %}
{% if pr_parts.size > 1 %}
{% assign before_url = pr_parts[0] %}
{% assign after_url = pr_parts[1] %}
{% assign url = "https://github.com/" | append: after_url %}
{% assign pr_number = after_url | split: "/pull/" %}
{% if pr_number.size > 1 %}
{% assign pr_num = pr_number[1] | split: " " | first %}
{% assign clickable_line = before_url | append: " in [#" | append: pr_num | append: "](" | append: url | append: ")" %}
{% assign processed_lines = processed_lines | append: clickable_line | append: "
" %}
{% else %}
{% assign processed_lines = processed_lines | append: line | append: "
" %}
{% endif %}
{% else %}
{% assign processed_lines = processed_lines | append: line | append: "
" %}
{% endif %}
{% else %}
{% assign processed_lines = processed_lines | append: line | append: "
" %}
{% endif %}
{% endfor %}
{% comment %} Add proper spacing between sections and bullet points {% endcomment %}
{% assign final_content = processed_lines | replace: "
### ", "
### " %}
{% assign final_content = final_content | replace: "
## ", "
## " %}
{% assign final_content = final_content | replace: "
- ", "
- " %}
{% comment %} Check if there are any bullet points (actual release notes) {% endcomment %}
{% if final_content contains "## " or final_content contains "- " %}
{{ final_content | markdownify }}
{% else %}
*No release notes available.*
{{ final_content | markdownify }}
{% endif %}
---
{% endfor %}
+1
View File
@@ -0,0 +1 @@
capod.darken.eu
+87 -76
View File
@@ -1,43 +1,48 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.784.0)
aws-sdk-core (3.177.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
aws-eventstream (1.3.2)
aws-partitions (1.1109.0)
aws-sdk-core (3.224.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.70.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.128.0)
aws-sdk-core (~> 3, >= 3.177.0)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.188.0)
aws-sdk-core (~> 3, >= 3.224.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.100.0)
faraday (1.10.3)
excon (0.109.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -53,27 +58,27 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.2.7)
fastlane (2.213.0)
fastimage (2.4.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@@ -82,33 +87,38 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.45.0)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -116,64 +126,66 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
google-cloud-storage (1.45.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-apis-storage_v1 (~> 0.29.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.6.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
json (2.7.6)
jwt (2.10.1)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.3.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.0)
public_suffix (5.0.1)
rake (13.0.6)
plist (3.7.2)
public_suffix (5.1.1)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
rubyzip (2.4.1)
security (0.1.5)
signet (0.18.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -181,34 +193,33 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
unf (0.2.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.22.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
universal-darwin-24
x86_64-linux
DEPENDENCIES
+7 -1
View File
@@ -1,5 +1,11 @@
---
layout: plain
permalink: /privacy
title: "Privacy Policy"
---
# Privacy policy
This is the privacy policy for the Android app "CAPod - Companion for AirPods".
This is the privacy policy for the Android app "CAPod - Companion for AirPods" by Matthias Urhahn (darken).
## Preamble
CAPod respects your privacy.
+8 -1
View File
@@ -1,6 +1,7 @@
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/banner.png" width="400">
# Companion App for AirPods (CAPod)
[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases)](https://github.com/d4rken-org/capod/releases/latest)
[![Code tests & eval](https://github.com/d4rken-org/capod/actions/workflows/code-checks.yml/badge.svg)](https://github.com/d4rken/capod/actions/workflows/code-checks.yml)
[![Crowdin](https://badges.crowdin.net/capod/localized.svg)](https://crowdin.com/project/capod)
@@ -26,6 +27,8 @@ Currently supported models:
* AirPods 1. Generation
* AirPods 2. Generation
* AirPods 3. Generation
* AirPods 4. Generation
* AirPods 4. Generation with ANC
* AirPods Pro 1. Generation
* AirPods Pro 2. Generation
* AirPods Pro 2. Generation (USB-C)
@@ -54,7 +57,6 @@ Currently supported models:
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [![](https://img.shields.io/f-droid/v/eu.darken.capod)](https://f-droid.org/en/packages/eu.darken.capod/) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
## Support the project
* Buy the CAPod Pro In-App purchase on [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
@@ -70,6 +72,7 @@ Currently supported models:
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
<img src="https://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
## Thanks to
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
@@ -78,6 +81,10 @@ Currently supported models:
Travis & Rye, Erik & Sipes, Brandon & Teplov, Sam. (2019). Handoff All Your Privacy A Review of Apples Bluetooth
Low Energy Continuity Protocol. Proceedings on Privacy Enhancing Technologies. 2019. 34-53. 10.2478/popets-2019-0057.
* Celosia, Guillaume. (2020). Privacy Challenges in Wireless Communications of the Internet of Things.
* The [MagicPods](https://github.com/steam3d/MagicPods-Windows) project. If you are looking for "CAPod for Windows",
check it out.
* [@kavishdevar](https://github.com/kavishdevar/librepods) and
his [LibrePods project](https://github.com/kavishdevar/librepods) for sharing a lot of cool stuff.
## License
+1 -1
View File
@@ -1 +1 @@
2.13.0-rc0 21300000
2.17.6-rc0 21706000
+26
View File
@@ -0,0 +1,26 @@
theme: minima
plugins:
- jekyll-relative-links
- jekyll-github-metadata
- jemoji
relative_links:
enabled: true
collections: true
title: "CAPod"
description: "A companion app for AirPods on Android."
author: "by Matthias Urhahn"
include:
- PRIVACY_POLICY.md
exclude:
- buildSrc
- gradle/wrapper
- fastlane
- Gemfile
- Gemfile.lock
- crowdin*
- app
- app-common
- app-wear
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="{{ page.lang | default: site.lang | default: 'en' }}">
{%- include head.html -%}
<body>
<main class="page-content" aria-label="Content">
<div class="wrapper">
{{ content }}
</div>
</main>
{%- include footer.html -%}
</body>
</html>
+2 -1
View File
@@ -1,6 +1,7 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("com.google.devtools.ksp")
id("kotlin-kapt")
id("kotlin-parcelize")
}
@@ -84,7 +85,7 @@ android {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
addBaseAndroid()
addBaseAndroidUi()
@@ -1,6 +1,6 @@
package eu.darken.capod.common
import java.util.*
import java.util.BitSet
fun Byte.toHex(): String = String.format("%02X", this)
fun UByte.toHex(): String = this.toByte().toHex()
@@ -18,3 +18,12 @@ fun UShort.isBitSet(pos: Int): Boolean = this.toShort().isBitSet(pos)
fun UShort.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(4, '0')
fun UByte.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(8, '0')
fun ByteArray.toHex(separator: String = "-"): String = joinToString(separator) { "%02X".format(it) }
fun String.fromHex(): ByteArray = this
.replace(" ", "")
.replace("-", "")
.also { require(it.length % 2 == 0) { "Not a HEX string, length: ${it.length}" } }
.chunked(2).map { it.toInt(16).toByte() }
.toByteArray()
@@ -1,5 +1,5 @@
package eu.darken.capod.common
object PrivacyPolicy {
const val URL = "https://github.com/d4rken-org/capod/blob/main/PRIVACY_POLICY.md"
const val URL = "https://capod.darken.eu/privacy"
}
@@ -2,10 +2,11 @@ package eu.darken.capod.common
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import javax.inject.Inject
@@ -15,13 +16,13 @@ class WebpageTool @Inject constructor(
) {
fun open(address: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(address)).apply {
val intent = Intent(Intent.ACTION_VIEW, address.toUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (e: Exception) {
log(ERROR) { "Failed to launch" }
log(ERROR) { "Failed to launch: ${e.asLog()}" }
}
}
@@ -9,13 +9,19 @@ import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.notifications.PendingIntentCompat
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -132,12 +138,12 @@ class BleScanner @Inject constructor(
ScannerMode.LOW_POWER -> {
setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
setMatchMode(ScanSettings.MATCH_MODE_STICKY)
setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
}
ScannerMode.BALANCED -> {
setScanMode(ScanSettings.SCAN_MODE_BALANCED)
setMatchMode(ScanSettings.MATCH_MODE_STICKY)
setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
}
ScannerMode.LOW_LATENCY -> {
setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
@@ -0,0 +1,3 @@
package eu.darken.capod.common.bluetooth
typealias BluetoothAddress = String
@@ -7,7 +7,7 @@ data class BluetoothDevice2(
internal val internal: BluetoothDevice,
val seenFirstAt: Instant,
) {
val address: String
val address: BluetoothAddress
get() = internal.address
val name: String?
@@ -3,6 +3,7 @@ package eu.darken.capod.common.bluetooth
import dagger.Reusable
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.fromHex
import java.time.Instant
import javax.inject.Inject
import kotlin.random.Random
@@ -25,7 +26,7 @@ class FakeBleData @Inject constructor(
address = "78:73:AF:B4:85:22",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".fromHex())
).run {
fakeDevices.add(this)
}
@@ -36,7 +37,7 @@ class FakeBleData @Inject constructor(
address = "78:73:FF:B4:85:5E",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".fromHex())
).run {
fakeDevices.add(this)
}
@@ -47,7 +48,7 @@ class FakeBleData @Inject constructor(
address = "4E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".fromHex())
).run {
fakeDevices.add(this)
}
@@ -57,7 +58,7 @@ class FakeBleData @Inject constructor(
address = "7E:E5:C7:65:D2:B5",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".fromHex())
).run {
fakeDevices.add(this)
}
@@ -67,7 +68,7 @@ class FakeBleData @Inject constructor(
address = "5E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".fromHex())
).run {
fakeDevices.add(this)
}
@@ -78,7 +79,7 @@ class FakeBleData @Inject constructor(
address = "5E:9E:D1:29:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".fromHex())
).run {
fakeDevices.add(this)
}
@@ -89,22 +90,11 @@ class FakeBleData @Inject constructor(
address = "6E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".fromHex())
).run {
fakeDevices.add(this)
}
return fakeDevices
}
private fun String.hexToByteArray(): ByteArray {
val trimmed = this
.replace(" ", "")
.replace(">", "")
.replace("<", "")
require(trimmed.length % 2 == 0) { "Not a HEX string" }
return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}
@@ -0,0 +1,14 @@
package eu.darken.capod.common.preferences
import android.content.Context
import android.util.AttributeSet
import androidx.preference.SwitchPreferenceCompat
class MaterialSwitchPreference(context: Context, attrs: AttributeSet?) :
SwitchPreferenceCompat(context, attrs) {
init {
// Use material switch
widgetLayoutResource = eu.darken.capod.common.R.layout.preference_material_switch
}
}
@@ -0,0 +1,16 @@
package eu.darken.capod.common.serialization
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@Suppress("unused")
@OptIn(ExperimentalEncodingApi::class)
class ByteArrayAdapter {
@ToJson
fun toJson(obj: ByteArray): String = Base64.encode(obj)
@FromJson
fun fromJson(base64: String): ByteArray? = Base64.decode(base64)
}
@@ -15,6 +15,7 @@ class SerializationModule {
@Singleton
fun moshi(): Moshi = Moshi.Builder()
.add(JavaInstantAdapter())
.add(ByteArrayAdapter())
.build()
}
@@ -3,7 +3,6 @@ package eu.darken.capod.common.uix
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.LiveData
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
@@ -16,8 +15,6 @@ abstract class Activity2 : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
log(tag, VERBOSE) { "onCreate(savedInstanceState=$savedInstanceState)" }
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows( window, false )
}
override fun onResume() {
@@ -7,8 +7,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
@@ -42,17 +40,6 @@ abstract class Fragment2(@LayoutRes val layoutRes: Int?) : Fragment(layoutRes ?:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
log(tag, VERBOSE) { "onViewCreated(view=$view, savedInstanceState=$savedInstanceState)" }
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val systemWindowInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(
v.paddingLeft,
systemWindowInsets.top,
v.paddingRight,
0
)
insets
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -5,12 +5,15 @@ import android.content.SharedPreferences
import androidx.preference.PreferenceDataStore
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.preferences.PreferenceStoreMapper
import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.common.preferences.createFlowPreference
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.IdentityResolvingKey
import eu.darken.capod.pods.core.apple.protocol.ProximityEncryptionKey
import javax.inject.Inject
import javax.inject.Singleton
@@ -24,14 +27,27 @@ class GeneralSettings @Inject constructor(
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi)
val useExtraMonitorNotification = preferences.createFlowPreference("core.monitor.notification.connected", false)
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi)
val showAll = preferences.createFlowPreference("core.showall.enabled", true)
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f)
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.20f)
val mainDeviceAddress = preferences.createFlowPreference<String?>("core.maindevice.address", null)
val mainDeviceAddress = preferences.createFlowPreference<BluetoothAddress?>("core.maindevice.address", null)
val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi)
val mainDeviceIdentityKey = preferences.createFlowPreference<IdentityResolvingKey?>(
"core.maindevice.identitykey",
null,
moshi
)
val mainDeviceEncryptionKey = preferences.createFlowPreference<ProximityEncryptionKey?>(
"core.maindevice.encryptionkey",
null,
moshi
)
val isOffloadedFilteringDisabled = preferences.createFlowPreference(
"core.compat.offloaded.filtering.disabled",
@@ -44,6 +60,7 @@ class GeneralSettings @Inject constructor(
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
monitorMode,
useExtraMonitorNotification,
scannerMode,
showAll,
minimumSignalQuality,
@@ -19,10 +19,18 @@ import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.PodFactory
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Duration
@@ -36,10 +44,10 @@ class PodMonitor @Inject constructor(
private val bleScanner: BleScanner,
private val podFactory: PodFactory,
private val generalSettings: GeneralSettings,
private val bluetoothManager: BluetoothManager2,
bluetoothManager: BluetoothManager2,
private val debugSettings: DebugSettings,
private val podDeviceCache: PodDeviceCache,
private val permissionTool: PermissionTool,
permissionTool: PermissionTool,
) {
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
@@ -121,6 +129,7 @@ class PodMonitor @Inject constructor(
log(TAG, WARN) { "Using unfiltered scan mode" }
setOf(ScanFilter.Builder().build())
}
else -> ProximityPairing.getBleScanFilter()
}
@@ -189,8 +198,14 @@ class PodMonitor @Inject constructor(
}
private fun determineMainDevice(pods: List<PodDevice>): PodDevice? {
val mainDeviceModel = generalSettings.mainDeviceModel.value
val identityKey = generalSettings.mainDeviceIdentityKey.value?.takeIf { it.isNotEmpty() }
if (identityKey != null) {
val irkHit = pods.filterIsInstance<ApplePods>().firstOrNull { it.flags.isIRKMatch }
log(TAG) { "IRK is configured, main device irkHit=$irkHit" }
return irkHit
}
val mainDeviceModel = generalSettings.mainDeviceModel.value
val presorted = sortPodsToInterest(pods).sortedByDescending {
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
}
@@ -6,6 +6,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import java.time.Duration
@@ -20,7 +21,7 @@ interface PodDevice {
val model: Model
val address: String
val address: BluetoothAddress
get() = scanResult.address
val seenLastAt: Instant
@@ -83,6 +84,14 @@ interface PodDevice {
"AirPods (Gen 3)",
R.drawable.devic_airpods_gen2_both,
),
@Json(name = "airpods.gen4") AIRPODS_GEN4(
"AirPods (Gen 4)",
R.drawable.devic_airpods_gen2_both,
),
@Json(name = "airpods.gen4.anc") AIRPODS_GEN4_ANC(
"AirPods (Gen 4 ANC)",
R.drawable.devic_airpods_gen2_both,
),
@Json(name = "airpods.pro") AIRPODS_PRO(
"AirPods Pro",
R.drawable.devic_airpods_pro2_both
@@ -99,6 +108,10 @@ interface PodDevice {
"AirPods Max",
R.drawable.devic_headphones_generic
),
@Json(name = "airpods.max.usbc") AIRPODS_MAX_USBC(
"AirPods Max USB-C",
R.drawable.devic_headphones_generic
),
@Json(name = "beats.flex") BEATS_FLEX(
"Beats Flex"
),
@@ -120,6 +133,9 @@ interface PodDevice {
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
"Power Beats Pro"
),
@Json(name = "beats.powerbeats.pro2") POWERBEATS_PRO2(
"Power Beats Pro 2"
),
@Json(name = "beats.fit.pro") BEATS_FIT_PRO(
"Beats Fit Pro"
),
@@ -5,10 +5,14 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
@@ -18,13 +22,16 @@ import javax.inject.Singleton
class AppleFactory @Inject constructor(
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
private val proximityPairingDecoder: ProximityPairing.Decoder,
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory<out ApplePods>>,
private val proximityMessageDecrypter: ProximityMessage.Decrypter,
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory>,
private val unknownAppleFactory: UnknownAppleDevice.Factory,
private val generalSettings: GeneralSettings,
private val rpaChecker: RPAChecker,
) {
private val lock = Mutex()
private fun getMessage(scanResult: BleScanResult): ProximityPairing.Message? {
private fun getMessage(scanResult: BleScanResult): ProximityMessage? {
val messages = try {
continuityProtocolDecoder.decode(scanResult)
} catch (e: Exception) {
@@ -50,13 +57,39 @@ class AppleFactory @Inject constructor(
}
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
val pm = getMessage(scanResult) ?: return@withLock null
val proximityMessage = getMessage(scanResult) ?: return@withLock null
val isIrkMatch = generalSettings.mainDeviceIdentityKey.value
?.let { rpaChecker.verify(scanResult.address, it) }
?: false
val payload = ProximityPayload(
public = ProximityPayload.Public(
proximityMessage.data.take(9).toUByteArray()
),
private = run {
if (!isIrkMatch) return@run null
if (proximityMessage.data.size != ProximityPairing.PAIRING_MESSAGE_LENGTH) return@run null
val encKey = generalSettings.mainDeviceEncryptionKey.value?.takeIf { it.isNotEmpty() }
if (encKey == null) return@run null
val encrypted = proximityMessage.data.takeLast(16).toUByteArray().toByteArray()
proximityMessageDecrypter.decrypt(encrypted, encKey)?.let {
ProximityPayload.Private(data = it)
}
},
)
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) }
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
return@withLock (factory ?: unknownAppleFactory).create(
scanResult = scanResult,
message = pm,
payload = payload,
flags = ApplePods.Flags(
isIRKMatch = isIrkMatch,
),
)
}
@@ -5,35 +5,60 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import eu.darken.capod.pods.core.apple.airpods.*
import eu.darken.capod.pods.core.apple.beats.*
import eu.darken.capod.pods.core.apple.misc.*
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen2
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen3
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen4
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen4Anc
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
import eu.darken.capod.pods.core.apple.airpods.AirPodsMaxUsbc
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2Usbc
import eu.darken.capod.pods.core.apple.beats.BeatsFitPro
import eu.darken.capod.pods.core.apple.beats.BeatsFlex
import eu.darken.capod.pods.core.apple.beats.BeatsSolo3
import eu.darken.capod.pods.core.apple.beats.BeatsStudio3
import eu.darken.capod.pods.core.apple.beats.BeatsX
import eu.darken.capod.pods.core.apple.beats.PowerBeats3
import eu.darken.capod.pods.core.apple.beats.PowerBeats4
import eu.darken.capod.pods.core.apple.beats.PowerBeatsPro
import eu.darken.capod.pods.core.apple.beats.PowerBeatsPro2
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen2
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro2
@InstallIn(SingletonComponent::class)
@Module
abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsGen4(factory: AirPodsGen4.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsGen4Anc(factory: AirPodsGen4Anc.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsMax2(factory: AirPodsMaxUsbc.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeats4(factory: PowerBeats4.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun powerBeats4(factory: PowerBeats4.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun powerBeatsPro2(factory: PowerBeatsPro2.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakeAirPodsPro2(factory: FakeAirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun fakeAirPodsPro2(factory: FakeAirPodsPro2.Factory): ApplePodsFactory
}
@@ -1,48 +1,62 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.toHex
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
interface ApplePods : PodDevice {
val proximityMessage: ProximityPairing.Message
val payload: ProximityPayload
val flags: Flags
// We start counting at the airpods prefix byte
val rawPrefix: UByte
get() = proximityMessage.data[0]
val pubPrefix: UByte
get() = payload.public.data[0]
val rawDeviceModel: UShort
get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort()
val pubDeviceModel: UShort
get() = (((payload.public.data[1].toInt() and 255) shl 8) or (payload.public.data[2].toInt() and 255)).toUShort()
val rawStatus: UByte
get() = proximityMessage.data[3]
val pubStatus: UByte
get() = payload.public.data[3]
val rawStatusHex: String
get() = rawStatus.toHex()
val pubPodsBattery: UByte
get() = payload.public.data[4]
val rawPodsBattery: UByte
get() = proximityMessage.data[4]
val pubFlags: UShort
get() = payload.public.data[5].upperNibble
val rawPodsBatteryHex: String
get() = rawPodsBattery.toHex()
val pubCaseBattery: UShort
get() = payload.public.data[5].lowerNibble
val rawFlags: UShort
get() = proximityMessage.data[5].upperNibble
val pubCaseLidState: UByte
get() = payload.public.data[6]
val rawCaseBattery: UShort
get() = proximityMessage.data[5].lowerNibble
val pubDeviceColor: UByte
get() = payload.public.data[7]
val rawCaseLidState: UByte
get() = proximityMessage.data[6]
val pubSuffix: UByte
get() = payload.public.data[8]
val rawDeviceColor: UByte
get() = proximityMessage.data[7]
data class BatteryState(
val isCharging: Boolean,
val level: Float,
)
val rawSuffix: UByte
get() = proximityMessage.data[8]
fun ProximityPayload.Private.asBatteryState(pos: Int): BatteryState? {
val raw = data[pos]
val level = (raw and 0x7Fu).toInt() / 100f
if (level < 0f || level > 1.0f) return null
val isCharging = (raw and 0x80u).toInt() != 0
return BatteryState(
isCharging = isCharging,
level = level
)
}
data class Flags(
val isIRKMatch: Boolean,
)
}
@@ -1,108 +1,26 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.collections.median
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Duration
import java.time.Instant
import kotlin.math.max
import eu.darken.capod.pods.core.apple.history.KnownDevice
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
interface ApplePodsFactory {
fun isResponsible(message: ProximityMessage): Boolean
fun create(scanResult: BleScanResult, payload: ProximityPayload, flags: ApplePods.Flags): ApplePods
data class Markings(
val vendor: UByte,
val length: UByte,
val device: UShort,
val podBatteryData: Set<UShort>,
val caseBatteryData: UShort,
val deviceColor: UByte,
data class ModelInfo(
val full: UShort,
val dirty: UByte,
)
private fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
length = ProximityPairing.PAIRING_MESSAGE_LENGTH.toUByte(),
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
// Make comparison order independent
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
caseBatteryData = data[5].lowerNibble,
deviceColor = data[7]
fun ProximityMessage.getModelInfo(): ModelInfo = ModelInfo(
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
dirty = data[1]
)
data class KnownDevice(
val id: PodDevice.Id,
val seenFirstAt: Instant,
val seenCounter: Int,
val history: List<ApplePods>,
val lastCaseBattery: Float?,
) {
val lastMessage: ProximityPairing.Message
get() = history.last().proximityMessage
val lastAddress: String
get() = history.last().address
val reliability: Float
get() {
if (history.size < 2) return 0f
val now = Instant.now()
val pingInterval = List(history.dropLast(1).size) { index ->
Duration.between(history[index].seenLastAt, history[index + 1].seenLastAt).toMillis()
}.map { it.toInt() }.median()
val expectedPings: Float = LOOKBACK.toMillis() / pingInterval.toFloat()
val recentPings = history.filter {
Duration.between(it.seenLastAt, now).toMillis() < LOOKBACK.toMillis() + pingInterval
}.size
val reliability: Float = (recentPings / expectedPings)
// log("#####") { "interval=$pingInterval, expectedPerMinute=$expectedPings, count=$recentPings" }
return max(0.0f, reliability).coerceAtMost(1f)
}
fun rssiSmoothed(latest: Int): Int {
val now = Instant.now()
return history
.filter { Duration.between(it.seenLastAt, now) < LOOKBACK }
.map { it.rssi }
.plus(latest)
.median()
}
fun isOlderThan(age: Duration): Boolean {
val now = Instant.now()
return Duration.between(history.last().seenLastAt, now) > age
}
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
companion object {
// 30 seconds at fastest interval
const val MAX_HISTORY = 60
val LOOKBACK = Duration.ofSeconds(30)
}
}
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
fun KnownDevice.getLatestCaseBattery(): Float? = this.lastCaseBattery
private fun Collection<ApplePods>.determineLatestCaseBattery(): Float? = this
.filterIsInstance<HasCase>()
.mapNotNull { it.batteryCasePercent }
.lastOrNull()
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
val definitive = setOf(
DualApplePods.LidState.OPEN,
@@ -118,89 +36,4 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
?.caseLidState
?: DualApplePods.LidState.NOT_IN_CASE
}
open fun historyTrimmer(
pods: List<ApplePods>
): List<ApplePods> {
return pods.takeLast(KnownDevice.MAX_HISTORY)
}
internal open fun searchHistory(current: PodType): KnownDevice? {
val scanResult = current.scanResult
val message = current.proximityMessage
knownDevices.values.toList().forEach { knownDevice ->
if (knownDevice.isOlderThan(Duration.ofSeconds(30))) {
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
knownDevices.remove(knownDevice.id)
}
}
knownDevices.values
.filter { it.history.size > KnownDevice.MAX_HISTORY }
.toList()
.forEach {
knownDevices[it.id] = it.copy(history = historyTrimmer(it.history))
}
var recognizedDevice: KnownDevice? = knownDevices.values
.firstOrNull { it.lastAddress == scanResult.address }
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
if (recognizedDevice == null) {
val currentMarkers = message.getApplePodsMarkings()
recognizedDevice = knownDevices.values
.firstOrNull { it.lastMessage.getApplePodsMarkings() == currentMarkers }
?.also { log(tag) { "searchHistory1: Close match based on similarity: $currentMarkers" } }
}
if (recognizedDevice == null) {
log(tag, WARN) { "searchHistory1: Didn't recognize: $message" }
}
return recognizedDevice
}
fun updateHistory(device: PodType) {
val existing = knownDevices[device.identifier]
knownDevices[device.identifier] = when {
existing != null -> {
val history = existing.history.plus(device)
existing.copy(
seenCounter = existing.seenCounter + 1,
history = history,
lastCaseBattery = history.determineLatestCaseBattery() ?: existing.lastCaseBattery
)
}
else -> {
log(tag) { "searchHistory1: Creating new history for $device" }
val history = listOf(device)
KnownDevice(
id = device.identifier,
seenFirstAt = device.seenFirstAt,
seenCounter = 1,
history = history,
lastCaseBattery = history.determineLatestCaseBattery()
)
}
}
}
data class ModelInfo(
val full: UShort,
val dirty: UByte,
)
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
dirty = data[1]
)
abstract fun isResponsible(message: ProximityPairing.Message): Boolean
abstract fun create(
scanResult: BleScanResult,
message: ProximityPairing.Message,
): ApplePods
}
@@ -4,14 +4,18 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.DualPodDevice.Pod
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasDualMicrophone
import eu.darken.capod.pods.core.HasEarDetectionDual
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
HasDualMicrophone, HasAppleColor {
val primaryPod: Pod
get() = when (rawStatus.isBitSet(5)) {
get() = when (pubStatus.isBitSet(5)) {
true -> Pod.LEFT
false -> Pod.RIGHT
}
@@ -21,13 +25,17 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
get() = !pubStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
payload.private?.asBatteryState(if (areValuesFlipped) 2 else 1)?.let {
return it.level
}
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
true -> pubPodsBattery.upperNibble.toInt()
false -> pubPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
@@ -42,9 +50,13 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
override val batteryRightPodPercent: Float?
get() {
payload.private?.asBatteryState(if (areValuesFlipped) 1 else 2)?.let {
return it.level
}
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
true -> pubPodsBattery.lowerNibble.toInt()
false -> pubPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
@@ -58,24 +70,24 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
}
val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
get() = pubStatus.isBitSet(6)
val isOnePodInCase: Boolean
get() = rawStatus.isBitSet(4)
get() = pubStatus.isBitSet(4)
val areBothPodsInCase: Boolean
get() = rawStatus.isBitSet(2)
get() = pubStatus.isBitSet(2)
override val isLeftPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
true -> pubStatus.isBitSet(3)
false -> pubStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
true -> pubStatus.isBitSet(1)
false -> pubStatus.isBitSet(3)
}
/**
@@ -83,45 +95,63 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
* For the pod that is in the case, this is flipped again though.
*/
override val isLeftPodMicrophone: Boolean
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
get() = pubStatus.isBitSet(5) xor isThisPodInThecase
/**
* The data flip bit is UNset if the right pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isRightPodMicrophone: Boolean
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
get() = !pubStatus.isBitSet(5) xor isThisPodInThecase
override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
get() {
payload.private?.asBatteryState(if (areValuesFlipped) 2 else 1)?.let {
return it.isCharging
}
return when (areValuesFlipped) {
false -> pubFlags.isBitSet(0)
true -> pubFlags.isBitSet(1)
}
}
override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
get() {
payload.private?.asBatteryState(if (areValuesFlipped) 1 else 2)?.let {
return it.isCharging
}
return when (areValuesFlipped) {
false -> pubFlags.isBitSet(1)
true -> pubFlags.isBitSet(0)
}
}
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> null
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
get() {
payload.private?.asBatteryState(3)?.let { return it.level }
return when (val value = pubCaseBattery.toInt()) {
15 -> null
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
get() {
payload.private?.asBatteryState(3)?.let { return it.isCharging }
return pubFlags.isBitSet(2)
}
val caseLidState: LidState
get() {
val rawstate = rawCaseLidState
return LidState.values().firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
val rawstate = pubCaseLidState
return LidState.entries.firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
}
/**
@@ -1,68 +0,0 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.pods.core.PodDevice
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
leftPodBattery = batteryLeftPodPercent,
rightPodBattery = batteryRightPodPercent,
microPhoneLeft = isLeftPodMicrophone,
microPhoneRight = isRightPodMicrophone,
chargingLeft = isLeftPodCharging,
chargingRight = isRightPodCharging,
color = rawDeviceColor,
model = model
)
/**
* Split pods, one in case, one not.
*/
data class SplitPodsMarkings(
val leftPodBattery: Float?,
val rightPodBattery: Float?,
val microPhoneLeft: Boolean,
val microPhoneRight: Boolean,
val chargingLeft: Boolean,
val chargingRight: Boolean,
val color: UByte,
val model: PodDevice.Model,
)
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
val target = device.getCaseMatchMarkings()
return filter { known ->
known.history
.filterIsInstance<DualApplePods>()
.any { it.getCaseMatchMarkings() == target }
}
}
override fun searchHistory(current: DualApplePods): KnownDevice? {
val basicResult = super.searchHistory(current)
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
log(tag, DEBUG) { "searchHistory2: Case ignored matches(${caseIgnored.size}): $caseIgnored" }
return when (caseIgnored.size) {
0 -> basicResult
1 -> caseIgnored.single()
else -> {
log(tag) { "searchHistory2: More than one result when ignoring case markers." }
val oldest = caseIgnored.maxByOrNull { it.history.size } ?: return null
caseIgnored.minus(oldest).forEach {
log(tag) { "searchHistory2: Removing outlier: $it" }
knownDevices.remove(it.id)
}
oldest
}
}
}
}
@@ -6,8 +6,8 @@ import eu.darken.capod.pods.core.HasPodStyle
interface HasAppleColor : ApplePods, HasPodStyle {
override val podStyle: HasPodStyle.PodStyle
get() = DeviceColor.values()
.firstOrNull { it.raw == rawDeviceColor }
get() = DeviceColor.entries
.firstOrNull { it.raw == pubDeviceColor }
?: DeviceColor.UNKNOWN
enum class DeviceColor(val raw: UByte?) : HasPodStyle.PodStyle {
@@ -10,13 +10,18 @@ import eu.darken.capod.pods.core.SinglePodDevice
interface SingleApplePods : ApplePods, SinglePodDevice, HasAppleColor {
override val batteryHeadsetPercent: Float?
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
15 -> null
else -> if (value > 10) {
log { "Headset above 100% battery: $value" }
1.0f
} else {
(value / 10f)
get() {
payload.private?.asBatteryState(1)?.let {
return it.level
}
return when (val value = pubPodsBattery.lowerNibble.toInt()) {
15 -> null
else -> if (value > 10) {
log { "Headset above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
}
@@ -1,3 +0,0 @@
package eu.darken.capod.pods.core.apple
abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory<SingleApplePods>(tag)
@@ -4,19 +4,23 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsGen1 constructor(
data class AirPodsGen1(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -34,19 +38,28 @@ data class AirPodsGen1 constructor(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsGen1(
scanResult = scanResult,
payload = payload,
flags = flags,
)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,19 +4,23 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsGen2 constructor(
data class AirPodsGen2(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -34,18 +38,28 @@ data class AirPodsGen2 constructor(
override val rssi: Int
get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsGen2(
scanResult = scanResult,
payload = payload,
flags = flags,
)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,19 +4,23 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsGen3 constructor(
data class AirPodsGen3(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -34,18 +38,24 @@ data class AirPodsGen3 constructor(
override val rssi: Int
get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsGen3(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -0,0 +1,80 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsGen4(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null
) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN4
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsGen4(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
repo.updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
private val DEVICE_CODE = 0x1920.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen4")
}
}
@@ -0,0 +1,80 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsGen4Anc(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null
) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN4_ANC
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsGen4Anc(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
repo.updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
private val DEVICE_CODE = 0x1B20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen4-ANC")
}
}
@@ -7,10 +7,13 @@ import eu.darken.capod.pods.core.HasChargeDetection
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.HasAppleColor
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -20,7 +23,8 @@ data class AirPodsMax(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
@@ -31,23 +35,34 @@ data class AirPodsMax(
get() = rssiAverage ?: super<SingleApplePods>.rssi
override val isHeadsetBeingCharged: Boolean
get() = rawFlags.isBitSet(0)
override val isBeingWorn: Boolean
get() = rawStatus.isBitSet(5)
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
get() {
payload.private?.asBatteryState(1)?.let {
return it.isCharging
}
return pubFlags.isBitSet(0)
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override val isBeingWorn: Boolean
get() = pubStatus.isBitSet(5)
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsMax(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -63,7 +78,7 @@ data class AirPodsMax(
}
companion object {
private val DEVICE_CODE_DIRTY = 10.toUByte()
private val DEVICE_CODE = 0x0A20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max")
}
}
@@ -0,0 +1,84 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.pods.core.HasChargeDetection
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.HasAppleColor
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsMaxUsbc(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX_USBC
override val rssi: Int
get() = rssiAverage ?: super<SingleApplePods>.rssi
override val isHeadsetBeingCharged: Boolean
get() {
payload.private?.asBatteryState(1)?.let {
return it.isCharging
}
return pubFlags.isBitSet(0)
}
override val isBeingWorn: Boolean
get() = pubStatus.isBitSet(5)
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsMaxUsbc(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
repo.updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
rssiAverage = result.rssiSmoothed(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x1F20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max", "USBC")
}
}
@@ -6,10 +6,13 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -19,7 +22,8 @@ data class AirPodsPro(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -53,18 +57,28 @@ data class AirPodsPro(
override val rssi: Int
get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsPro(
scanResult = scanResult,
payload = payload,
flags = flags,
)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -79,7 +93,6 @@ data class AirPodsPro(
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
@@ -6,10 +6,13 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -19,7 +22,8 @@ data class AirPodsPro2(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -53,18 +57,24 @@ data class AirPodsPro2(
override val rssi: Int
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsPro2(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsPro2(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -6,10 +6,13 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -19,7 +22,8 @@ data class AirPodsPro2Usbc(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -53,18 +57,24 @@ data class AirPodsPro2Usbc(
override val rssi: Int
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsPro2Usbc(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsPro2Usbc(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -9,7 +9,7 @@ import eu.darken.capod.pods.core.apple.ApplePods
interface HasStateDetectionAirPods : HasStateDetection, ApplePods {
override val state: ConnectionState
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
get() = ConnectionState.entries.firstOrNull { pubSuffix == it.raw } ?: ConnectionState.UNKNOWN
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,7 +19,8 @@ data class BeatsFitPro(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -34,18 +38,24 @@ data class BeatsFitPro(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = BeatsFitPro(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = BeatsFitPro(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,7 +19,8 @@ data class BeatsFlex(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods {
@@ -26,18 +30,24 @@ data class BeatsFlex(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = BeatsFlex(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,7 +19,8 @@ data class BeatsSolo3(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods {
@@ -25,18 +29,24 @@ data class BeatsSolo3(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = BeatsSolo3(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,25 +19,32 @@ data class BeatsStudio3(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods {
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = BeatsStudio3(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,7 +19,8 @@ data class BeatsX(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods {
@@ -26,18 +30,24 @@ data class BeatsX(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = BeatsX(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = BeatsX(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,7 +19,8 @@ data class PowerBeats3(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods {
@@ -26,18 +30,24 @@ data class PowerBeats3(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = PowerBeats3(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = PowerBeats3(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,10 +4,13 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -17,7 +20,8 @@ data class PowerBeats4(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasStateDetectionAirPods {
@@ -27,18 +31,24 @@ data class PowerBeats4(
override val rssi: Int
get() = rssiAverage ?: super<SingleApplePods>.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = PowerBeats4(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = PowerBeats4(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -4,9 +4,12 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -16,7 +19,8 @@ data class PowerBeatsPro(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
@@ -34,18 +38,24 @@ data class PowerBeatsPro(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = PowerBeatsPro(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -64,7 +74,7 @@ data class PowerBeatsPro(
}
companion object {
private val DEVICE_CODE_DIRTY = 11.toUByte()
private val DEVICE_CODE = 0x0B20.toUShort()
private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "Pro")
}
}
@@ -0,0 +1,80 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class PowerBeatsPro2(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null
) : DualApplePods {
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO2
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = PowerBeatsPro2(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
repo.updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
private val DEVICE_CODE = 0x1D20.toUShort()
private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "Pro2")
}
}
@@ -0,0 +1,68 @@
package eu.darken.capod.pods.core.apple.history
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.collections.median
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Duration
import java.time.Instant
import kotlin.math.max
data class KnownDevice(
val id: PodDevice.Id,
val seenFirstAt: Instant,
val seenCounter: Int,
val history: List<ApplePods>,
val lastCaseBattery: Float?,
) {
val lastPayload: ProximityPayload
get() = history.last().payload
val lastAddress: BluetoothAddress
get() = history.last().address
val reliability: Float
get() {
if (history.size < 2) return 0f
val now = Instant.now()
val pingInterval = List(history.dropLast(1).size) { index ->
Duration.between(history[index].seenLastAt, history[index + 1].seenLastAt).toMillis()
}.map { it.toInt() }.median()
val expectedPings: Float = LOOKBACK.toMillis() / pingInterval.toFloat()
val recentPings = history.filter {
Duration.between(it.seenLastAt, now).toMillis() < LOOKBACK.toMillis() + pingInterval
}.size
val reliability: Float = (recentPings / expectedPings)
// log("#####") { "interval=$pingInterval, expectedPerMinute=$expectedPings, count=$recentPings" }
return max(0.0f, reliability).coerceAtMost(1f)
}
fun rssiSmoothed(latest: Int): Int {
val now = Instant.now()
return history
.filter { Duration.between(it.seenLastAt, now) < LOOKBACK }
.map { it.rssi }
.plus(latest)
.median()
}
fun isOlderThan(age: Duration): Boolean {
val now = Instant.now()
return Duration.between(history.last().seenLastAt, now) > age
}
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
companion object {
// 30 seconds at fastest interval
const val MAX_HISTORY = 60
val LOOKBACK = Duration.ofSeconds(30)
}
}
@@ -0,0 +1,178 @@
package eu.darken.capod.pods.core.apple.history
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
import java.time.Duration
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PodHistoryRepo @Inject constructor(
private val generalSettings: GeneralSettings,
private val rpaChecker: RPAChecker,
) {
private val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
data class Identifier(
val device: UShort,
val podBatteryData: Set<UShort>,
val caseBatteryData: UShort,
val deviceColor: UByte,
)
private fun ProximityPayload.getFuzzyIdentifier(): Identifier = Identifier(
device = (((public.data[1].toInt() and 255) shl 8) or (public.data[2].toInt() and 255)).toUShort(),
// Make comparison order independent
podBatteryData = setOf(public.data[4].upperNibble, public.data[4].lowerNibble),
caseBatteryData = public.data[5].lowerNibble,
deviceColor = public.data[7]
)
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
leftPodBattery = batteryLeftPodPercent,
rightPodBattery = batteryRightPodPercent,
microPhoneLeft = isLeftPodMicrophone,
microPhoneRight = isRightPodMicrophone,
chargingLeft = isLeftPodCharging,
chargingRight = isRightPodCharging,
color = pubDeviceColor,
model = model
)
/**
* Split pods, one in case, one not.
*/
data class SplitPodsMarkings(
val leftPodBattery: Float?,
val rightPodBattery: Float?,
val microPhoneLeft: Boolean,
val microPhoneRight: Boolean,
val chargingLeft: Boolean,
val chargingRight: Boolean,
val color: UByte,
val model: PodDevice.Model,
)
fun search(current: ApplePods): KnownDevice? {
val basicResult = baseSearch(current)
val caseIgnored = if (current is DualApplePods) {
val target = current.getCaseMatchMarkings()
knownDevices.values.filter { known ->
known.history.filterIsInstance<DualApplePods>().any { it.getCaseMatchMarkings() == target }
}
} else {
emptySet()
}
log(TAG, DEBUG) { "search2: Case ignored matches(${caseIgnored.size}): $caseIgnored" }
return when (caseIgnored.size) {
0 -> basicResult
1 -> caseIgnored.single()
else -> {
log(TAG) { "search2: More than one result when ignoring case markers." }
val oldest = caseIgnored.maxByOrNull { it.history.size } ?: return null
caseIgnored.minus(oldest).forEach {
log(TAG) { "search2: Removing outlier: $it" }
knownDevices.remove(it.id)
}
oldest
}
}
}
private fun baseSearch(current: ApplePods): KnownDevice? {
val scanResult = current.scanResult
val payload = current.payload
// Timeout
knownDevices.values.toList()
.filter { it.isOlderThan(Duration.ofSeconds(30)) }
.forEach { knownDevice ->
log(TAG, VERBOSE) { "search1: Removing stale known device: $knownDevice" }
knownDevices.remove(knownDevice.id)
}
// Trim history
knownDevices.values
.filter { it.history.size > KnownDevice.MAX_HISTORY }
.toList()
.forEach { knownDevices[it.id] = it.copy(history = it.history.takeLast(KnownDevice.MAX_HISTORY)) }
var recognizedDevice: KnownDevice? = knownDevices.values
.firstOrNull { it.lastAddress == scanResult.address }
?.also { log(TAG, VERBOSE) { "search1: Recovered previous ID via address: $it" } }
if (recognizedDevice == null) {
generalSettings.mainDeviceIdentityKey.value?.let { key ->
if (!rpaChecker.verify(current.address, key)) {
log(TAG, VERBOSE) { "search1: Current device does not match IRK" }
return@let
}
recognizedDevice = knownDevices.values
.firstOrNull { rpaChecker.verify(it.lastAddress, key) }
.also { log(TAG, VERBOSE) { "search1: Recovered previous ID via IRK: $it" } }
}
}
if (recognizedDevice == null) {
val currentMarkers = payload.getFuzzyIdentifier()
recognizedDevice = knownDevices.values
.firstOrNull { it.lastPayload.getFuzzyIdentifier() == currentMarkers }
?.also { log(TAG) { "search1: Close match based on similarity: $currentMarkers" } }
}
if (recognizedDevice == null) log(TAG, WARN) { "search1: Didn't recognize: $current" }
return recognizedDevice
}
fun updateHistory(device: ApplePods) {
val existing = knownDevices[device.identifier]
fun Collection<ApplePods>.determineLatestCaseBattery(): Float? = this
.filterIsInstance<HasCase>()
.mapNotNull { it.batteryCasePercent }
.lastOrNull()
knownDevices[device.identifier] = if (existing != null) {
val history = existing.history.plus(device)
existing.copy(
seenCounter = existing.seenCounter + 1,
history = history,
lastCaseBattery = history.determineLatestCaseBattery() ?: existing.lastCaseBattery
)
} else {
log(TAG) { "Creating new history for $device" }
val history = listOf(device)
KnownDevice(
id = device.identifier,
seenFirstAt = device.seenFirstAt,
seenCounter = 1,
history = history,
lastCaseBattery = history.determineLatestCaseBattery()
)
}
}
companion object {
private val TAG = logTag("Pod", "Apple", "Factory", "History")
}
}
@@ -1,15 +1,17 @@
package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -18,118 +20,46 @@ import javax.inject.Inject
* Similar data structure but a lot of placeholder values or hardcoded values
* Marketed as TWS i99999
*/
data class FakeAirPodsGen1 constructor(
data class FakeAirPodsGen1(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
override val rssi: Int
get() = rssiAverage ?: super<ApplePods>.rssi
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
private val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
override val isLeftPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
}
override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
}
override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
}
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
override val rssi: Int
get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen1>(TAG) {
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = FakeAirPodsGen1(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = FakeAirPodsGen1(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -1,133 +1,63 @@
package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
/**
* Similar data structure but a lot of placeholder values or hardcoded values
*/
data class FakeAirPodsGen2 constructor(
data class FakeAirPodsGen2(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
override val rssi: Int
get() = rssiAverage ?: super<ApplePods>.rssi
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
private val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
override val isLeftPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
}
override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
}
override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
}
get() = rssiAverage ?: super<DualApplePods>.rssi
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen2>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = FakeAirPodsGen2(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = FakeAirPodsGen2(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -1,15 +1,17 @@
package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -18,118 +20,46 @@ import javax.inject.Inject
* Shorter data structure.
* https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308
*/
data class FakeAirPodsGen3 constructor(
data class FakeAirPodsGen3(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
override val rssi: Int
get() = rssiAverage ?: super<ApplePods>.rssi
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
}
override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
}
private val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
override val isLeftPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
}
get() = rssiAverage ?: super<DualApplePods>.rssi
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen3>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = FakeAirPodsGen3(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = FakeAirPodsGen3(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -3,15 +3,17 @@ package eu.darken.capod.pods.core.apple.misc
import androidx.annotation.DrawableRes
import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -20,17 +22,18 @@ import javax.inject.Inject
* Shorter data structure.
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
*/
data class FakeAirPodsPro constructor(
data class FakeAirPodsPro(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
) : DualApplePods, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
@@ -51,103 +54,30 @@ data class FakeAirPodsPro constructor(
get() = R.drawable.devic_airpods_pro2_right
override val rssi: Int
get() = rssiAverage ?: super<ApplePods>.rssi
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
}
override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
}
private val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
override val isLeftPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
}
get() = rssiAverage ?: super<DualApplePods>.rssi
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = FakeAirPodsPro(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = FakeAirPodsPro(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -3,16 +3,19 @@ package eu.darken.capod.pods.core.apple.misc
import androidx.annotation.DrawableRes
import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -20,18 +23,19 @@ import javax.inject.Inject
* AirPods Pro2 clone
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
*/
data class FakeAirPodsPro2 constructor(
data class FakeAirPodsPro2(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null,
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
private val cachedCaseState: LidState? = null,
) : DualApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
@@ -52,99 +56,32 @@ data class FakeAirPodsPro2 constructor(
get() = R.drawable.devic_airpods_pro2_right
override val rssi: Int
get() = rssiAverage ?: scanResult.rssi
private val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
private val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
override val isLeftPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
}
override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
}
override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
}
get() = rssiAverage ?: super<DualApplePods>.rssi
override val caseLidState: LidState
get() = cachedCaseState ?: super.caseLidState
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro2>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = FakeAirPodsPro2(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = FakeAirPodsPro2(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -7,7 +7,9 @@ import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
@@ -17,7 +19,8 @@ data class UnknownAppleDevice(
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = 0f,
private val rssiAverage: Int? = null,
) : ApplePods {
@@ -29,18 +32,21 @@ data class UnknownAppleDevice(
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = true
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = true
override fun create(
scanResult: BleScanResult,
message: ProximityPairing.Message,
payload: ProximityPayload,
flags: ApplePods.Flags,
): ApplePods {
var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
var basic = UnknownAppleDevice(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
repo.updateHistory(basic)
if (result == null) return basic
@@ -0,0 +1,3 @@
package eu.darken.capod.pods.core.apple.protocol
typealias IdentityResolvingKey = ByteArray
@@ -0,0 +1,3 @@
package eu.darken.capod.pods.core.apple.protocol
typealias ProximityEncryptionKey = ByteArray
@@ -0,0 +1,58 @@
package eu.darken.capod.pods.core.apple.protocol
import android.R.id.message
import android.annotation.SuppressLint
import dagger.Reusable
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import okio.ByteString.Companion.toByteString
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
data class ProximityMessage(
val type: UByte,
val length: Int,
val data: UByteArray
) {
override fun toString(): String {
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
}
@Reusable
class Decrypter @Inject constructor() {
@SuppressLint("GetInstance")
fun decrypt(data: ByteArray, key: ProximityEncryptionKey): UByteArray? {
val decryptedData = try {
val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply {
val secretKey = SecretKeySpec(key, "AES")
init(Cipher.DECRYPT_MODE, secretKey)
}
cipher.doFinal(data.copyOfRange(data.size - 16, data.size))
} catch (e: Exception) {
log(
TAG,
Logging.Priority.ERROR
) { "Failed to decrypt $message with ${key.toByteString()}\n${e.asLog()}" }
null
}
log(
TAG,
Logging.Priority.VERBOSE
) { "Decrypted $message with ${key.toByteString()} to ${decryptedData?.toByteString()}" }
if (decryptedData == null || decryptedData.size != 16) return null
return decryptedData.toUByteArray()
}
companion object {
private val TAG = logTag("Pod", "Factory", "Decrypter")
}
}
}
@@ -8,27 +8,15 @@ import javax.inject.Inject
object ProximityPairing {
data class Message(
val type: UByte,
val length: Int,
val data: UByteArray
) {
override fun toString(): String {
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
}
}
@Reusable
class Decoder @Inject constructor() {
fun decode(message: ContinuityProtocol.Message): Message? {
fun decode(message: ContinuityProtocol.Message): ProximityMessage? {
if (message.type != CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING) {
log { "Not a proximity pairing message: $this" }
return null
}
return Message(
return ProximityMessage(
type = message.type,
length = message.length,
data = message.data
@@ -0,0 +1,15 @@
package eu.darken.capod.pods.core.apple.protocol
data class ProximityPayload(
val public: Public,
val private: Private?,
) {
data class Public(
val data: UByteArray,
)
data class Private(
val data: UByteArray,
)
}
@@ -0,0 +1,52 @@
package eu.darken.capod.pods.core.apple.protocol
import android.annotation.SuppressLint
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import okio.ByteString.Companion.toByteString
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
class RPAChecker @Inject constructor() {
// Resolvable-Private-Address
fun verify(address: BluetoothAddress, irk: IdentityResolvingKey): Boolean = try {
val rpa = address.split(":").map { it.toInt(16).toByte() }.reversed().toByteArray()
val prand = rpa.copyOfRange(3, 6)
val hash = rpa.copyOfRange(0, 3)
val computedHash = ah(irk, prand)
hash.contentEquals(computedHash)
} catch (e: Exception) {
log(
TAG,
Logging.Priority.ERROR
) { "Failed to verify RPA\naddress=${address}\nIRK=${irk.toByteString()}\n${e.asLog()}" }
false
}
// E function (Encryption function):
@SuppressLint("GetInstance")
private fun e(key: ByteArray, data: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply {
val secretKey = SecretKeySpec(key.reversedArray(), "AES")
init(Cipher.ENCRYPT_MODE, secretKey)
}
return cipher.doFinal(data.reversedArray()).reversedArray()
}
// AH function (Address Hashing function):
private fun ah(k: ByteArray, r: ByteArray): ByteArray {
val rPadded = ByteArray(16).apply {
r.copyInto(this, 0, 0, 3)
}
return e(k, rPadded).copyOfRange(0, 3)
}
companion object {
private val TAG = logTag("Monitor", "PodMonitor", "RPAChecker")
}
}
@@ -1,5 +1,6 @@
package eu.darken.capod.reaction.core.popup
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
@@ -51,8 +52,8 @@ class PopUpReaction @Inject constructor(
return@mapNotNull null
}
log(TAG, VERBOSE) {
val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) }
val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) }
val prev = previous?.pubCaseLidState?.let { String.format("%02X", it.toByte()) }
val cur = current.pubCaseLidState.let { String.format("%02X", it.toByte()) }
"previous=$prev (${previous?.caseLidState}), current=$cur (${current.caseLidState})"
}
log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" }
@@ -109,7 +110,7 @@ class PopUpReaction @Inject constructor(
else -> null
}
private val connectionCoolDowns = mutableMapOf<String, Instant>()
private val connectionCoolDowns = mutableMapOf<BluetoothAddress, Instant>()
private fun monitorConnection(): Flow<Event> = reactionSettings.showPopUpOnConnection.flow
.flatMapLatest { isEnabled ->
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M23 4.5C23 6.43 21.43 8 19.5 8S16 6.43 16 4.5 17.57 1 19.5 1 23 2.57 23 4.5M19.5 10C16.47 10 14 7.53 14 4.5C14 4 14.08 3.5 14.21 3H5C3.89 3 3 3.89 3 5V19C3 20.11 3.9 21 5 21H19C20.11 21 21 20.11 21 19V9.79C20.5 9.92 20 10 19.5 10Z" />
</vector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false" />
@@ -1,2 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N.v.t.</string>
<string name="general_error_label">Fout</string>
<string name="general_grant_permission_action">Gee toestemming</string>
<string name="overview_nomaindevice_label">Geen primêre toestel</string>
<string name="overview_nomaindevice_description">Alle bespeurde toestelle is waarskynlik nie joune nie. Skakel jou toestel aan en koppel dit of pas die instellings aan.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth is gedeaktiveer</string>
<string name="overview_bluetooth_disabled_description">Bluetooth is gedeaktiveer, aktiveer dit ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth-verbinding</string>
<string name="permission_bluetooth_connect_description">Hierdie toepassing vereis die "Bluetooth-verbinding"-toestemming om met gekoppelde toestelle te kommunikeer en verbindings te inisieer.</string>
<string name="permission_bluetooth_scan_label">Bluetooth-skandering</string>
<string name="permission_bluetooth_scan_description">Die "Bluetooth-skandering"-toestemming laat hierdie toepassing toe om Bluetooth-data van nabygeleë toestelle soos jou AirPods te ontdek en te ontvang.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Hierdie toepassing vereis die "Bluetooth"-toestemming om aan gekoppelde Bluetooth-toestelle te koppel.</string>
<string name="permission_access_fine_location_label">Toegang tot presiese ligging</string>
<string name="permission_access_fine_location_description">CAPod gebruik die "presiese ligging"-toestemming om Bluetooth Lae Energie-data te ontvang. Jou oorfone gebruik Bluetooth Lae Energie-tegnologie om hul status uit te saai. Hierdie toepassing sal NIE Bluetooth-data gebruik om jou ligging te bepaal NIE.</string>
<string name="permission_background_location_label">Toegang tot agtergrondligging</string>
<string name="permission_background_location_description">CAPod gebruik "toegang tot agtergrondligging" om funksies soos "Wys opspringer" en "Autokoppel" moontlik te maak terwyl die toepassing gesluit is. Toegang tot agtergrondligging laat die toepassing toe om Bluetooth Lae Energie-data te ontvang terwyl dit in die agtergrond is. Hierdie toepassing sal NIE Bluetooth-data gebruik om jou ligging te bepaal NIE.</string>
<string name="permission_ignore_battery_optimizations_label">Deaktiveer batteryoptimalisering</string>
<string name="permission_ignore_battery_optimizations_description">Batteryoptimalisering verhoed dat hierdie toepassing betroubaar Bluetooth-data ontvang terwyl die toepassing in die agtergrond is.</string>
<string name="permission_required_title">Die volgende toestemming word vereis:</string>
<string name="permission_system_alert_window_label">Stelselwaarskuwingsvenster</string>
<string name="permission_system_alert_window_description">Laat CAPod toe om bo-oor ander toepassings te teken om die funksie "Wys PopUp" moontlik te maak.</string>
<string name="settings_scanner_mode_lowpower_label">Lae krag</string>
<string name="settings_scanner_mode_balanced_label">Gebalanseerd</string>
<string name="settings_scanner_mode_lowlatency_label">Lae latensie</string>
<string name="settings_monitor_mode_manual_label">Wanneer toepassing oop is</string>
<string name="settings_monitor_mode_automatic_label">Wanneer toestel gekoppel is</string>
<string name="settings_monitor_mode_always_label">Altyd</string>
<string name="settings_reaction_autoconnect_whenseen_label">Wanneer gesien</string>
<string name="settings_reaction_autoconnect_caseopen_label">Houer is oop</string>
<string name="settings_reaction_autoconnect_inear_label">In oor</string>
<string name="pods_dual_left_label">Linker pod</string>
<string name="pods_dual_right_label">Regter pod</string>
<string name="pods_case_label">Houer</string>
<string name="pods_case_status_open_label">Oop</string>
<string name="pods_case_status_closed_label">Gesluit</string>
<string name="pods_connection_state_disconnected_label">Nie aan \'n toestel gekoppel nie</string>
<string name="pods_connection_state_idle_label">Gekoppel aan \'n toestel, maar ledig</string>
<string name="pods_connection_state_music_label">In musiekmodus</string>
<string name="pods_connection_state_call_label">In oproepmodus</string>
<string name="pods_connection_state_ringing_label">Lui</string>
<string name="pods_connection_state_hanging_up_label">Besig om af te haak</string>
<string name="pods_connection_state_unknown_label">Onbekende verbindingstatus</string>
<string name="pods_unknown_raw_data_label">Rou data</string>
<string name="pods_unknown_label">Onbekende toestel</string>
<string name="pods_unknown_contact_dev">Hierdie is \'n onbekende toestel, maar dit gebruik \'n soortgelyke boodskapformaat. Kom ons voeg ondersteuning daarvoor by, kontak my :)</string>
<string name="pods_none_label_short">Geen toestel</string>
<string name="pods_charging_label">Laai tans</string>
<string name="pods_inear_label">In oor</string>
<string name="pods_microphone_label">Mikrofoon</string>
<string name="pods_yours">Joune</string>
<string name="headset_being_worn_label">Word gedra</string>
<string name="headset_not_being_worn_label">Word nie gedra nie</string>
<string name="pods_case_unknown_state">Onbekende status</string>
<string name="last_seen_x">Laas gesien: %s</string>
<string name="first_seen_x">Eerste gesien: %s</string>
<string name="permission_post_notifications_label">Wys kennisgewings</string>
<string name="permission_post_notifications_description">Laat CAPod toe om kennisgewings oor jou AirPods te wys, bv. hul huidige status terwyl hulle gekoppel is.</string>
</resources>
+63 -1
View File
@@ -1,2 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">የለም</string>
<string name="general_error_label">ስህተት</string>
<string name="general_grant_permission_action">ፍቃድ ስጥ</string>
<string name="overview_nomaindevice_label">ዋና መሣሪያ የለም</string>
<string name="overview_nomaindevice_description">የተገኙት ሁሉም መሣሪያዎች የእርስዎ ላይሆኑ ይችላሉ። መሣሪያዎን ያብሩና ያገናኙ ወይም ቅንብሮቹን ያስተካክሉ።</string>
<string name="overview_bluetooth_disabled_label">ብሉቱዝ ጠፍቷል</string>
<string name="overview_bluetooth_disabled_description">ብሉቱዝ ጠፍቷል፣ ያብሩት ;)</string>
<string name="permission_bluetooth_connect_label">የብሉቱዝ ግንኙነት</string>
<string name="permission_bluetooth_connect_description">ይህ መተግበሪያ ከተጣመሩ መሣሪያዎች ጋር ለመገናኘት እና ግንኙነቶችን ለመጀመር የ \"ብሉቱዝ ግንኙነት\" ፍቃድ ያስፈልገዋል።</string>
<string name="permission_bluetooth_scan_label">የብሉቱዝ ቅኝት</string>
<string name="permission_bluetooth_scan_description">የ \"ብሉቱዝ ቅኝት\" ፍቃድ ይህ መተግበሪያ እንደ የእርስዎ AirPods ካሉ በአቅራቢያ ካሉ መሣሪያዎች የብሉቱዝ መረጃን እንዲያገኝ እና እንዲቀበል ያስችለዋል።</string>
<string name="permission_bluetooth_label">ብሉቱዝ</string>
<string name="permission_bluetooth_description">ይህ መተግበሪያ ከተጣመሩ የብሉቱዝ መሣሪያዎች ጋር ለመገናኘት የ \"ብሉቱዝ\" ፍቃድ ያስፈልገዋል።</string>
<string name="permission_access_fine_location_label">ትክክለኛ አካባቢን ይድረሱ</string>
<string name="permission_access_fine_location_description">CAPod የብሉቱዝ ዝቅተኛ ኃይል መረጃን ለመቀበል የ \"ትክክለኛ አካባቢ\" ፍቃድን ይጠቀማል። የእርስዎ የጆሮ ማዳመጫዎች ሁኔታቸውን ለማሰራጨት የብሉቱዝ ዝቅተኛ ኃይል ቴክኖሎጂን ይጠቀማሉ። ይህ መተግበሪያ አካባቢዎን ለመወሰን የብሉቱዝ መረጃን አይጠቀምም።</string>
<string name="permission_background_location_label">የዳራ አካባቢ መዳረሻ</string>
<string name="permission_background_location_description">CAPod መተግበሪያው ሲዘጋ እንደ \"ብቅ-ባይ አሳይ\" እና \"ራስ-ሰር ግንኙነት\" ያሉ ባህሪያትን ለማንቃት \"የዳራ አካባቢ መዳረሻ\"ን ይጠቀማል። የዳራ አካባቢ መዳረሻ መተግበሪያው በዳራ ውስጥ እያለ የብሉቱዝ ዝቅተኛ ኃይል መረጃን እንዲቀበል ያስችለዋል። ይህ መተግበሪያ አካባቢዎን ለመወሰን የብሉቱዝ መረጃን አይጠቀምም።</string>
<string name="permission_ignore_battery_optimizations_label">የባትሪ ማመቻቸትን አሰናክል</string>
<string name="permission_ignore_battery_optimizations_description">የባትሪ ማመቻቸት ይህ መተግበሪያ በዳራ ውስጥ እያለ የብሉቱዝ መረጃን በአስተማማኝ ሁኔታ እንዳይቀበል ይከለክላል።</string>
<string name="permission_required_title">የሚከተለው ፍቃድ ያስፈልጋል፦</string>
<string name="permission_system_alert_window_label">የسርዓት ማንቂያ መስኮት</string>
<string name="permission_system_alert_window_description">CAPod \"ብቅ-ባይ አሳይ\" ባህሪን እንዲቻል ለማድረግ በሌሎች መተግበሪያዎች ላይ እንዲስል ይፍቀዱለት።</string>
<string name="settings_scanner_mode_lowpower_label">ዝቅተኛ ኃይል</string>
<string name="settings_scanner_mode_balanced_label">ሚዛናዊ</string>
<string name="settings_scanner_mode_lowlatency_label">ዝቅተኛ መዘግየት</string>
<string name="settings_monitor_mode_manual_label">መተግበሪያው ሲከፈት</string>
<string name="settings_monitor_mode_automatic_label">መሣሪያው ሲገናኝ</string>
<string name="settings_monitor_mode_always_label">ሁልጊዜ</string>
<string name="settings_reaction_autoconnect_whenseen_label">ሲታይ</string>
<string name="settings_reaction_autoconnect_caseopen_label">ኬዝ ክፍት ነው</string>
<string name="settings_reaction_autoconnect_inear_label">በጆሮ ውስጥ</string>
<string name="pods_dual_left_label">ግራ ፖድ</string>
<string name="pods_dual_right_label">ቀኝ ፖድ</string>
<string name="pods_case_label">ኬዝ</string>
<string name="pods_case_status_open_label">ክፍት</string>
<string name="pods_case_status_closed_label">ዝግ</string>
<string name="pods_connection_state_disconnected_label">ከመሳሪያ ጋር አልተገናኘም</string>
<string name="pods_connection_state_idle_label">ከመሳሪያ ጋር ተገናኝቷል、ግን ስራ ፈት</string>
<string name="pods_connection_state_music_label">በሙዚቃ ሁነታ</string>
<string name="pods_connection_state_call_label">በጥሪ ሁነታ</string>
<string name="pods_connection_state_ringing_label">እየጠራ</string>
<string name="pods_connection_state_hanging_up_label">እየዘጋ</string>
<string name="pods_connection_state_unknown_label">ያልታወቀ የግንኙነት ሁኔታ</string>
<string name="pods_unknown_raw_data_label">ጥሬ መረጃ</string>
<string name="pods_unknown_label">ያልታወቀ መሣሪያ</string>
<string name="pods_unknown_contact_dev">ይህ ያልታወቀ መሣሪያ ነው、ግን ተመሳሳይ የመልእክት ቅርጸት እየተጠቀመ ነው። ለእሱ ድጋፍ እንጨምርበት、አግኙኝ :)</string>
<string name="pods_none_label_short">መሣሪያ የለም</string>
<string name="pods_charging_label">ኃይል እየሞላ</string>
<string name="pods_inear_label">በጆሮ ውስጥ</string>
<string name="pods_microphone_label">ማይክሮፎን</string>
<string name="pods_yours">የእርስዎ</string>
<string name="headset_being_worn_label">እየተለበሰ</string>
<string name="headset_not_being_worn_label">አልተለበሰም</string>
<string name="pods_case_unknown_state">ያልታወቀ ሁኔታ</string>
<string name="last_seen_x">ለመጨረሻ ጊዜ የታየው፦ %s</string>
<string name="first_seen_x">ለመጀመሪያ ጊዜ የታየው፦ %s</string>
<string name="permission_post_notifications_label">ማሳወቂያዎችን አሳይ</string>
<string name="permission_post_notifications_description">"CAPod ስለ የእርስዎ AirPods ማሳወቂያዎችን እንዲያሳይ ይፍቀዱለት፣ ለምሳሌ ሲገናኙ የአሁኑ ሁኔታቸው።"</string>
</resources>
@@ -55,9 +55,10 @@
<string name="pods_microphone_label">ميكروفون</string>
<string name="pods_yours">الخاصة بك</string>
<string name="headset_being_worn_label">يتم ارتداؤها</string>
<string name="headset_not_being_worn_label">لا يتم ارتداؤها</string>
<string name="pods_case_unknown_state">حالة غير معروفة</string>
<string name="last_seen_x">آخر ظهور: %s</string>
<string name="first_seen_x">أول ظهور:%s</string>
<string name="permission_post_notifications_label"></string>
<string name="permission_post_notifications_label">إظهار الإشعارات</string>
<string name="permission_post_notifications_description">"اسمح لـلتطبيق بإظهار الإشعارات المتعلقة بسماعات الأذن الخاصة بك، على سبيل المثال. حالتهم الحالية خلال الاتصال."</string>
</resources>
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Mövcud deyil</string>
<string name="general_error_label">Xəta</string>
<string name="general_grant_permission_action">İcazə ver</string>
@@ -11,6 +15,7 @@
<string name="permission_bluetooth_connect_description">Bu tətbiq, cütləşdirilmiş cihazlarla qarşılıqlı əlaqədə olmaq və bağlantıları başlatmaq üçün \"Bluetooth bağlantısı\" icazəsini tələb edir.</string>
<string name="permission_bluetooth_scan_label">Bluetooth skan etmə</string>
<string name="permission_bluetooth_scan_description">\"Bluetooth skan etmə\" icazəsi, tətbiqə AirPods kimi yaxınlıqda olan cihazlardan Bluetooth verilənlərini almağa və kəşf etməyə icazə verir.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Bu tətbiq, cütləşdirilmiş bluetooth cihazlarına bağlanmaq üçün \"Bluetooth\" icazəsini tələb edir.</string>
<string name="permission_access_fine_location_label">Həssas yerləşməyə müraciət</string>
<string name="permission_access_fine_location_description">CAPod, Bluetooth Zəif Enerji verilənlərini almaq üçün \"dəqiq yerləşmə\" icazəsini istifadə edir. Qulaqlıqlarınız, öz vəziyyətlərini (qalan batereya faizini) yayımlamaq Bluetooth Zəif Enerji texnologiyasını istifadə edir. Bu tətbiq, Bluetooth verilənlərini yerləşmənizi müəyyənləşdirmək üçün İSTİFADƏ ETMİR.</string>
@@ -51,7 +56,10 @@
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Sizin</string>
<string name="headset_being_worn_label">Taxılır</string>
<string name="headset_not_being_worn_label">Taxılmayıb</string>
<string name="pods_case_unknown_state">Bilinməyən vəziyyət</string>
<string name="last_seen_x">Son görünmə: %s</string>
<string name="first_seen_x">İlk görünmə: %s</string>
<string name="permission_post_notifications_label">Bildirişləri göstər</string>
<string name="permission_post_notifications_description">CAPod-a AirPods-larınız haqqında bildirişlər göstərməyə icazə verin, məsələn, qoşulmuş vəziyyətdə olduqları zaman onların mövcud statusu.</string>
</resources>
@@ -3,6 +3,7 @@
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Н</string>
<string name="general_error_label">Памылка</string>
<string name="general_grant_permission_action">Даць дазвол</string>
@@ -13,7 +14,7 @@
<string name="permission_bluetooth_connect_label">Злучэнне па Bluetooth</string>
<string name="permission_bluetooth_connect_description">Гэтай праграме патрабуецца дазвол на выкарыстанне Bluetooth для ўзаемадзеяння са спалучанымі прыладамі і стварэння новых злучэнняў.</string>
<string name="permission_bluetooth_scan_label">Сканіраванне Bluetooth</string>
<string name="permission_bluetooth_scan_description">Дазвол на сканіраванне Bluetooth дасць магчымасць гэтай праграме выяўляць і атрымліваць даныя па Bluetooth з навакольных прылад, такіх як вашы AirPods.</string>
<string name="permission_bluetooth_scan_description">Дазвол на сканіраванне Bluetooth дасць магчымасць гэтай праграме выяўляць і атрымлівать даныя па Bluetooth з навакольных прылад, такіх як вашы AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Гэтай праграме патрабуецца дазвол на выкарыстанне Bluetooth для злучэння са спалучанымі прыладамі.</string>
<string name="permission_access_fine_location_label">Доступ да дакладнага месцазнаходжання</string>
@@ -55,6 +56,7 @@
<string name="pods_microphone_label">Мікрафон</string>
<string name="pods_yours">Вашы</string>
<string name="headset_being_worn_label">Надзеты</string>
<string name="headset_not_being_worn_label">Не надзеты</string>
<string name="pods_case_unknown_state">Невядомы стан</string>
<string name="last_seen_x">Апошні раз быў: %s</string>
<string name="first_seen_x">Упершыню быў: %s</string>
+71 -1
View File
@@ -1,2 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Няма данни</string>
<string name="general_error_label">Грешка</string>
<string name="general_grant_permission_action">Даване на разрешение</string>
<string name="overview_nomaindevice_label">Няма основно устройство</string>
<string name="overview_nomaindevice_description">Всички открити устройства вероятно не са ваши. Включете и свържете вашето устройство или коригирайте настройките.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth е деактивиран</string>
<string name="overview_bluetooth_disabled_description">Bluetooth е деактивиран, активирайте го ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth свързване</string>
<string name="permission_bluetooth_connect_description">Това приложение изисква разрешението \"Bluetooth свързване\" за взаимодействие със сдвоени устройства и иницииране на връзки.</string>
<string name="permission_bluetooth_scan_label">Bluetooth сканиране</string>
<string name="permission_bluetooth_scan_description">Разрешението \"Bluetooth сканиране\" позволява на това приложение да открива и получава Bluetooth данни от близки устройства като вашите AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Това приложение изисква разрешението \"Bluetooth\" за свързване със сдвоени Bluetooth устройства.</string>
<string name="permission_access_fine_location_label">Достъп до точно местоположение</string>
<string name="permission_access_fine_location_description">CAPod използва разрешението \"точно местоположение\" за получаване на Bluetooth Low Energy данни. Вашите слушалки използват Bluetooth Low Energy технология за излъчване на състоянието си. Това приложение НЯМА да използва Bluetooth данни за определяне на вашето местоположение.</string>
<string name="permission_background_location_label">Достъп до местоположение във фонов режим</string>
<string name="permission_background_location_description">CAPod използва \"достъп до местоположение във фонов режим\", за да активира функции като \"Показване на изскачащ прозорец\" и \"Автоматично свързване\", докато приложението е затворено. Достъпът до местоположение във фонов режим позволява на приложението да получава Bluetooth Low Energy данни, докато е във фонов режим. Това приложение НЯМА да използва Bluetooth данни за определяне на вашето местоположение.</string>
<string name="permission_ignore_battery_optimizations_label">Деактивиране на оптимизациите на батерията</string>
<string name="permission_ignore_battery_optimizations_description">Оптимизациите на батерията пречат на това приложение да получава надеждно Bluetooth данни, докато приложението е във фонов режим.</string>
<string name="permission_required_title">Изисква се следното разрешение:</string>
<string name="permission_system_alert_window_label">Прозорец за системни предупреждения</string>
<string name="permission_system_alert_window_description">Разрешете на CAPod да рисува върху други приложения, за да направи възможна функцията \"Показване на PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Ниска мощност</string>
<string name="settings_scanner_mode_balanced_label">Балансиран</string>
<string name="settings_scanner_mode_lowlatency_label">Ниска латентност</string>
<string name="settings_monitor_mode_manual_label">Когато приложението е отворено</string>
<string name="settings_monitor_mode_automatic_label">Когато устройството е свързано</string>
<string name="settings_monitor_mode_always_label">Винаги</string>
<string name="settings_reaction_autoconnect_whenseen_label">Когато се види</string>
<string name="settings_reaction_autoconnect_caseopen_label">Калъфът е отворен</string>
<string name="settings_reaction_autoconnect_inear_label">В ухото</string>
<string name="pods_dual_left_label">Лява слушалка</string>
<string name="pods_dual_right_label">Дясна слушалка</string>
<string name="pods_case_label">Калъф</string>
<string name="pods_case_status_open_label">Отворен</string>
<string name="pods_case_status_closed_label">Затворен</string>
<string name="pods_connection_state_disconnected_label">Не е свързано с устройство</string>
<string name="pods_connection_state_idle_label">Свързано с устройство, но неактивно</string>
<string name="pods_connection_state_music_label">В музикален режим</string>
<string name="pods_connection_state_call_label">В режим на повикване</string>
<string name="pods_connection_state_ringing_label">Звънене</string>
<string name="pods_connection_state_hanging_up_label">Затваряне</string>
<string name="pods_connection_state_unknown_label">Неизвестно състояние на връзката</string>
<string name="pods_unknown_raw_data_label">Сурови данни</string>
<string name="pods_unknown_label">Неизвестно устройство</string>
<string name="pods_unknown_contact_dev">Това е неизвестно устройство, но използва подобен формат на съобщенията. Нека добавим поддръжка за него, свържете се с мен :)</string>
<string name="pods_none_label_short">Няма устройство</string>
<string name="pods_charging_label">Зареждане</string>
<string name="pods_inear_label">В ухото</string>
<string name="pods_microphone_label">Микрофон</string>
<string name="pods_yours">Ваши</string>
<string name="headset_being_worn_label">Носи се</string>
<string name="headset_not_being_worn_label">Не се носи</string>
<string name="pods_case_unknown_state">Неизвестно състояние</string>
<string name="last_seen_x">Последно видяно: %s</string>
<string name="first_seen_x">Първо видяно: %s</string>
<string name="permission_post_notifications_label">Показване на известия</string>
<string name="permission_post_notifications_description">Разрешете на CAPod да показва известия за вашите AirPods, напр. текущото им състояние, докато са свързани.</string>
</resources>
@@ -1,4 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">প্রযোজ্য নয়</string>
<string name="general_error_label">ত্রুটি</string>
<string name="general_grant_permission_action">অনুমতি প্রদান করুন</string>
<string name="overview_nomaindevice_label">কোনও প্রাথমিক ডিভাইস নেই</string>
<string name="overview_nomaindevice_description">শনাক্ত করা সমস্ত ডিভাইস সম্ভবত আপনার নয়। আপনার ডিভাইস চালু করুন এবং সংযোগ করুন অথবা সেটিংস সামঞ্জস্য করুন।</string>
<string name="overview_bluetooth_disabled_label">ব্লুটুথ নিষ্ক্রিয় করা আছে</string>
<string name="overview_bluetooth_disabled_description">ব্লুটুথ নিষ্ক্রিয় করা আছে, এটি সক্রিয় করুন ;)</string>
<string name="permission_bluetooth_connect_label">ব্লুটুথ সংযোগ</string>
<string name="permission_bluetooth_connect_description">এই অ্যাপটির পেয়ার করা ডিভাইসগুলির সাথে ইন্টারঅ্যাক্ট করতে এবং সংযোগ শুরু করতে "ব্লুটুথ সংযোগ" অনুমতির প্রয়োজন।</string>
<string name="permission_bluetooth_scan_label">ব্লুটুথ স্ক্যানিং</string>
<string name="permission_bluetooth_scan_description">"ব্লুটুথ স্ক্যানিং" অনুমতি এই অ্যাপটিকে আপনার AirPods-এর মতো কাছাকাছি ডিভাইসগুলি থেকে ব্লুটুথ ডেটা আবিষ্কার এবং গ্রহণ করতে দেয়।</string>
<string name="permission_bluetooth_label">ব্লুটুথ</string>
<string name="permission_bluetooth_description">এই অ্যাপটির পেয়ার করা ব্লুটুথ ডিভাইসগুলির সাথে সংযোগ করতে "ব্লুটুথ" অনুমতির প্রয়োজন।</string>
<string name="permission_access_fine_location_label">সঠিক অবস্থানে অ্যাক্সেস</string>
<string name="permission_access_fine_location_description">CAPod ব্লুটুথ লো এনার্জি ডেটা গ্রহণ করতে "সঠিক অবস্থান" অনুমতি ব্যবহার করে। আপনার হেডফোনগুলি তাদের স্থিতি সম্প্রচার করতে ব্লুটুথ লো এনার্জি প্রযুক্তি ব্যবহার করে। এই অ্যাপটি আপনার অবস্থান নির্ধারণ করতে ব্লুটুথ ডেটা ব্যবহার করবে না।</string>
<string name="permission_background_location_label">পটভূমি অবস্থানে অ্যাক্সেস</string>
<string name="permission_background_location_description">অ্যাপটি বন্ধ থাকা অবস্থায় "পপআপ দেখান" এবং "স্বয়ংক্রিয় সংযোগ" এর মতো বৈশিষ্ট্যগুলি সক্ষম করতে CAPod "পটভূমি অবস্থানে অ্যাক্সেস" ব্যবহার করে। পটভূমি অবস্থানে অ্যাক্সেস অ্যাপটিকে পটভূমিতে থাকাকালীন ব্লুটুথ লো এনার্জি ডেটা গ্রহণ করতে দেয়। এই অ্যাপটি আপনার অবস্থান নির্ধারণ করতে ব্লুটুথ ডেটা ব্যবহার করবে না।</string>
<string name="permission_ignore_battery_optimizations_label">ব্যাটারি অপ্টিমাইজেশন নিষ্ক্রিয় করুন</string>
<string name="permission_ignore_battery_optimizations_description">অ্যাপটি পটভূমিতে থাকাকালীন ব্যাটারি অপ্টিমাইজেশন এই অ্যাপটিকে নির্ভরযোগ্যভাবে ব্লুটুথ ডেটা গ্রহণ করতে বাধা দেয়।</string>
<string name="permission_required_title">নিম্নলিখিত অনুমতি প্রয়োজন:</string>
<string name="permission_system_alert_window_label">সিস্টেম সতর্কতা উইন্ডো</string>
<string name="permission_system_alert_window_description">CAPod-কে "পপআপ দেখান" বৈশিষ্ট্যটি সম্ভব করতে অন্যান্য অ্যাপের উপর আঁকতে অনুমতি দিন।</string>
<string name="settings_scanner_mode_lowpower_label">কম শক্তি</string>
<string name="settings_scanner_mode_balanced_label">ভারসাম্যপূর্ণ</string>
<string name="settings_scanner_mode_lowlatency_label">কম লেটেন্সি</string>
<string name="settings_monitor_mode_manual_label">অ্যাপ খোলা থাকলে</string>
<string name="settings_monitor_mode_automatic_label">ডিভাইস সংযুক্ত থাকলে</string>
<string name="settings_monitor_mode_always_label">সবসময়</string>
<string name="settings_reaction_autoconnect_whenseen_label">দেখলে</string>
<string name="settings_reaction_autoconnect_caseopen_label">কেস খোলা আছে</string>
<string name="settings_reaction_autoconnect_inear_label">কানে</string>
<string name="pods_dual_left_label">বাম পড</string>
<string name="pods_dual_right_label">ডান পড</string>
<string name="pods_case_label">কেস</string>
<string name="pods_case_status_open_label">খোলা</string>
<string name="pods_case_status_closed_label">বন্ধ</string>
<string name="pods_connection_state_disconnected_label">কোনও ডিভাইসের সাথে সংযুক্ত নয়</string>
<string name="pods_connection_state_idle_label">একটি ডিভাইসের সাথে সংযুক্ত, কিন্তু নিষ্ক্রিয়</string>
<string name="pods_connection_state_music_label">মিউজিক মোডে</string>
<string name="pods_connection_state_call_label">কল মোডে</string>
<string name="pods_connection_state_ringing_label">রিং হচ্ছে</string>
<string name="pods_connection_state_hanging_up_label">সংযোগ বিচ্ছিন্ন হচ্ছে</string>
<string name="pods_connection_state_unknown_label">অজানা সংযোগ স্থিতি</string>
<string name="pods_unknown_raw_data_label">কাঁচা ডেটা</string>
<string name="pods_unknown_label">অজানা ডিভাইস</string>
<string name="pods_unknown_contact_dev">এটি একটি অজানা ডিভাইস, তবে এটি একই ধরনের বার্তা বিন্যাস ব্যবহার করছে। আসুন এটির জন্য সমর্থন যোগ করি, আমার সাথে যোগাযোগ করুন :)</string>
<string name="pods_none_label_short">কোনও ডিভাইস নেই</string>
<string name="pods_charging_label">চার্জ হচ্ছে</string>
<string name="pods_inear_label">কানে</string>
<string name="pods_microphone_label">মাইক্রোফোন</string>
<string name="pods_yours">আপনার</string>
<string name="headset_being_worn_label">পরিধান করা হচ্ছে</string>
<string name="headset_not_being_worn_label">পরিধান করা হয়নি</string>
<string name="pods_case_unknown_state">অজানা স্থিতি</string>
<string name="last_seen_x">শেষ দেখা: %s</string>
<string name="first_seen_x">প্রথম দেখা: %s</string>
<string name="permission_post_notifications_label">বিজ্ঞপ্তি দেখান</string>
<string name="permission_post_notifications_description">CAPod-কে আপনার AirPods সম্পর্কে বিজ্ঞপ্তি দেখাতে অনুমতি দিন, যেমন সংযুক্ত থাকাকালীন তাদের বর্তমান স্থিতি।</string>
</resources>
@@ -55,6 +55,7 @@
<string name="pods_microphone_label">Micròfon</string>
<string name="pods_yours">Vostres</string>
<string name="headset_being_worn_label">En ús</string>
<string name="headset_not_being_worn_label">No s\'utilitzen</string>
<string name="pods_case_unknown_state">Estat desconegut</string>
<string name="last_seen_x">Últim cop vist: %s</string>
<string name="first_seen_x">Primera vegada vist: %s</string>
@@ -55,6 +55,7 @@
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Vaše</string>
<string name="headset_being_worn_label">Používá se</string>
<string name="headset_not_being_worn_label">Nepoužíváno</string>
<string name="pods_case_unknown_state">Neznámý stav</string>
<string name="last_seen_x">Naposledy spatřeno: %s</string>
<string name="first_seen_x">Poprvé spatřeno: %s</string>
+71 -1
View File
@@ -1,2 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">I/T</string>
<string name="general_error_label">Fejl</string>
<string name="general_grant_permission_action">Giv tilladelse</string>
<string name="overview_nomaindevice_label">Ingen primær enhed</string>
<string name="overview_nomaindevice_description">Alle registrerede enheder er sandsynligvis ikke dine. Tænd og tilslut din enhed, eller juster indstillingerne.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth er deaktiveret</string>
<string name="overview_bluetooth_disabled_description">Bluetooth er deaktiveret, aktiver det ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth-forbindelse</string>
<string name="permission_bluetooth_connect_description">Denne app kræver tilladelsen \"Bluetooth-forbindelse\" for at interagere med parrede enheder og starte forbindelser.</string>
<string name="permission_bluetooth_scan_label">Bluetooth-scanning</string>
<string name="permission_bluetooth_scan_description">Tilladelsen \"Bluetooth-scanning\" giver denne app mulighed for at finde og modtage Bluetooth-data fra nærliggende enheder såsom dine AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Denne app kræver tilladelsen \"Bluetooth\" for at oprette forbindelse til parrede Bluetooth-enheder.</string>
<string name="permission_access_fine_location_label">Adgang til fin placering</string>
<string name="permission_access_fine_location_description">CAPod bruger tilladelsen \"fin placering\" til at modtage Bluetooth Low Energy-data. Dine hovedtelefoner bruger Bluetooth Low Energy-teknologi til at udsende deres status. Denne app vil IKKE bruge Bluetooth-data til at bestemme din placering.</string>
<string name="permission_background_location_label">Adgang til baggrundsplacering</string>
<string name="permission_background_location_description">CAPod bruger \"adgang til baggrundsplacering\" til at aktivere funktioner som \"Vis pop op\" og \"AutoConnect\", mens appen er lukket. Adgang til baggrundsplacering giver appen mulighed for at modtage Bluetooth Low Energy-data, mens den er i baggrunden. Denne app vil IKKE bruge Bluetooth-data til at bestemme din placering.</string>
<string name="permission_ignore_battery_optimizations_label">Deaktiver batterioptimeringer</string>
<string name="permission_ignore_battery_optimizations_description">Batterioptimeringer forhindrer denne app i at modtage Bluetooth-data pålideligt, mens appen er i baggrunden.</string>
<string name="permission_required_title">Følgende tilladelse er påkrævet:</string>
<string name="permission_system_alert_window_label">Systemadvarselsvindue</string>
<string name="permission_system_alert_window_description">Tillad CAPod at tegne oven på andre apps for at gøre funktionen \"Vis PopOp\" mulig.</string>
<string name="settings_scanner_mode_lowpower_label">Lav effekt</string>
<string name="settings_scanner_mode_balanced_label">Balanceret</string>
<string name="settings_scanner_mode_lowlatency_label">Lav latenstid</string>
<string name="settings_monitor_mode_manual_label">Når appen er åben</string>
<string name="settings_monitor_mode_automatic_label">Når enheden er tilsluttet</string>
<string name="settings_monitor_mode_always_label">Altid</string>
<string name="settings_reaction_autoconnect_whenseen_label">Når set</string>
<string name="settings_reaction_autoconnect_caseopen_label">Etui er åbent</string>
<string name="settings_reaction_autoconnect_inear_label">I øret</string>
<string name="pods_dual_left_label">Venstre pod</string>
<string name="pods_dual_right_label">Højre pod</string>
<string name="pods_case_label">Etui</string>
<string name="pods_case_status_open_label">Åben</string>
<string name="pods_case_status_closed_label">Lukket</string>
<string name="pods_connection_state_disconnected_label">Ikke tilsluttet en enhed</string>
<string name="pods_connection_state_idle_label">Tilsluttet en enhed, men inaktiv</string>
<string name="pods_connection_state_music_label">I musiktilstand</string>
<string name="pods_connection_state_call_label">I opkaldstilstand</string>
<string name="pods_connection_state_ringing_label">Ringer</string>
<string name="pods_connection_state_hanging_up_label">Lægger på</string>
<string name="pods_connection_state_unknown_label">Ukendt forbindelsestilstand</string>
<string name="pods_unknown_raw_data_label">Rådata</string>
<string name="pods_unknown_label">Ukendt enhed</string>
<string name="pods_unknown_contact_dev">Dette er en ukendt enhed, men den bruger et lignende meddelelsesformat. Lad os tilføje understøttelse for den, kontakt mig :)</string>
<string name="pods_none_label_short">Ingen enhed</string>
<string name="pods_charging_label">Oplader</string>
<string name="pods_inear_label">I øret</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Dine</string>
<string name="headset_being_worn_label">Bæres</string>
<string name="headset_not_being_worn_label">Bæres ikke</string>
<string name="pods_case_unknown_state">Ukendt tilstand</string>
<string name="last_seen_x">Sidst set: %s</string>
<string name="first_seen_x">Først set: %s</string>
<string name="permission_post_notifications_label">Vis notifikationer</string>
<string name="permission_post_notifications_description">Tillad CAPod at vise notifikationer om dine AirPods, f.eks. deres aktuelle status, mens de er tilsluttet.</string>
</resources>
@@ -24,15 +24,15 @@
<string name="permission_ignore_battery_optimizations_description">Batterie Optimierung hindert die App daran Bluetooth Daten zu erhalten während die App im Hintergrund ist.</string>
<string name="permission_required_title">Die folgende Genehmigung ist erforderlich:</string>
<string name="permission_system_alert_window_label">Systemwarnungsfenster</string>
<string name="permission_system_alert_window_description">Erlauben Sie CAPod, über andere Apps zu zeichnen, um die Funktion \"PopUp anzeigen\" zu ermöglichen.</string>
<string name="permission_system_alert_window_description">Erlauben Sie CAPod, PopUp Benachrichtigungen über anderen Apps anzuzeigen.</string>
<string name="settings_scanner_mode_lowpower_label">Geringer Strom</string>
<string name="settings_scanner_mode_balanced_label">Ausgewogen</string>
<string name="settings_scanner_mode_lowlatency_label">Geringe Latenz</string>
<string name="settings_monitor_mode_manual_label">Wenn die App geöffnet ist</string>
<string name="settings_monitor_mode_automatic_label">Wenn das Gerät verbunden ist</string>
<string name="settings_monitor_mode_always_label">Stets</string>
<string name="settings_monitor_mode_always_label">Immer</string>
<string name="settings_reaction_autoconnect_whenseen_label">Wenn gesehen</string>
<string name="settings_reaction_autoconnect_caseopen_label">Fall ist offen</string>
<string name="settings_reaction_autoconnect_caseopen_label">Ladecase ist offen</string>
<string name="settings_reaction_autoconnect_inear_label">Im Ohr</string>
<string name="pods_dual_left_label">Linker Pod</string>
<string name="pods_dual_right_label">Rechter Pod</string>
@@ -44,7 +44,7 @@
<string name="pods_connection_state_music_label">Im Musik Modus</string>
<string name="pods_connection_state_call_label">Im Anruf Modus</string>
<string name="pods_connection_state_ringing_label">Klingelt</string>
<string name="pods_connection_state_hanging_up_label">Legt auf</string>
<string name="pods_connection_state_hanging_up_label">Auflegen</string>
<string name="pods_connection_state_unknown_label">Unbekannter Verbindungsstatus</string>
<string name="pods_unknown_raw_data_label">Rohe Daten</string>
<string name="pods_unknown_label">Unbekanntes Gerät</string>
@@ -55,6 +55,7 @@
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Dein</string>
<string name="headset_being_worn_label">Wird getragen</string>
<string name="headset_not_being_worn_label">Wird nicht getragen</string>
<string name="pods_case_unknown_state">Unbekannter Zustand</string>
<string name="last_seen_x">Zuletzt gesehen: %s</string>
<string name="first_seen_x">Zum ersten Mal gesehen: %s</string>
@@ -3,7 +3,7 @@
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Δε διατίθεται</string>
<string name="general_value_not_available_label">Μ/Δ</string>
<string name="general_error_label">Σφάλμα</string>
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
<string name="overview_nomaindevice_label">Καμία κύρια συσκευή</string>
@@ -23,7 +23,7 @@
<string name="permission_ignore_battery_optimizations_label">Απενεργοποίηση βελτιστοποιήσεων μπαταρίας</string>
<string name="permission_ignore_battery_optimizations_description">Οι βελτιστοποιήσεις μπαταρίας εμποδίζουν αυτήν την εφαρμογή να λαμβάνει αξιόπιστα δεδομένα Bluetooth ενώ η εφαρμογή βρίσκεται στο παρασκήνιο.</string>
<string name="permission_required_title">Η ακόλουθη άδεια είναι απαραίτητη:</string>
<string name="permission_system_alert_window_label">Παράθυρο ειδοποίησης συστήματος</string>
<string name="permission_system_alert_window_label">Παράθυρο Ειδοποίησης Συστήματος</string>
<string name="permission_system_alert_window_description">Επιτρέψτε στο CAPod να εμφανίζεται πάνω από άλλες εφαρμογές για να είναι δυνατή η λειτουργία \"Εμφάνιση αναδυόμενου\".</string>
<string name="settings_scanner_mode_lowpower_label">Χαμηλή ισχύς</string>
<string name="settings_scanner_mode_balanced_label">Εξισορρόπηση</string>
@@ -46,7 +46,7 @@
<string name="pods_connection_state_ringing_label">Κουδούνισμα</string>
<string name="pods_connection_state_hanging_up_label">Τερματισμός κλήσης</string>
<string name="pods_connection_state_unknown_label">Άγνωστη κατάσταση σύνδεσης</string>
<string name="pods_unknown_raw_data_label">Ανεπεξέργαστα δεδομένα</string>
<string name="pods_unknown_raw_data_label">Ακατέργαστα δεδομένα</string>
<string name="pods_unknown_label">Άγνωστη συσκευή</string>
<string name="pods_unknown_contact_dev">Αυτή είναι μια άγνωστη συσκευή, αλλά χρησιμοποιεί παρόμοια μορφή μηνυμάτων. Ας προσθέσουμε υποστήριξη για αυτήν, επικοινωνήστε μαζί μου :)</string>
<string name="pods_none_label_short">Καμία συσκευή</string>
@@ -55,6 +55,7 @@
<string name="pods_microphone_label">Μικρόφωνο</string>
<string name="pods_yours">Δικό σας</string>
<string name="headset_being_worn_label">Φοριέται</string>
<string name="headset_not_being_worn_label">Δε φοριέται</string>
<string name="pods_case_unknown_state">Άγνωστη κατάσταση</string>
<string name="last_seen_x">Τελευταία εμφάνιση: %s</string>
<string name="first_seen_x">Πρώτη εμφάνιση: %s</string>
@@ -3,8 +3,70 @@
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N/D</string>
<string name="general_error_label">Error</string>
<string name="general_grant_permission_action">Conceder permiso</string>
<string name="overview_nomaindevice_label">Ningún dispositivo principal</string>
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean tuyos. Encendé y conectá tu dispositivo o ajustá la configuración.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth desactivado</string>
<string name="overview_bluetooth_disabled_description">Bluetooth está desactivado, activalo ;)</string>
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicación necesita el permiso \"Conexión Bluetooth\" para interactuar con los dispositivos emparejados e iniciar conexiones.</string>
<string name="permission_bluetooth_scan_label">Escaneo de Bluetooth</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo de Bluetooth\" permite que esta aplicación detecte y reciba datos de Bluetooth de dispositivos cercanos, como tus AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Esta aplicación requiere el permiso \"Bluetooth\" para conectarse a dispositivos Bluetooth emparejados.</string>
<string name="permission_access_fine_location_label">Acceso a ubicación precisa</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso \"ubicación precisa\" para recibir datos de Bluetooth de baja energía. Tus auriculares utilizan la tecnología Bluetooth de baja energía para transmitir su estado. Esta aplicación NO utilizará datos de Bluetooth para determinar tu ubicación.</string>
<string name="permission_background_location_label">Acceso a la ubicación en segundo plano</string>
<string name="permission_background_location_description">CAPod utiliza \"acceso a la ubicación en segundo plano\" para habilitar características como \"Mostrar pop-up\" y \"Conexión automática\" mientras la aplicación está cerrada. El acceso a la ubicación en segundo plano permite que la aplicación reciba datos de Bluetooth de baja energía mientras está en segundo plano. Esta aplicación NO utilizará datos de Bluetooth para determinar tu ubicación.</string>
<string name="permission_ignore_battery_optimizations_label">Desactivar optimizaciones de batería</string>
<string name="permission_ignore_battery_optimizations_description">Las optimizaciones de batería impiden que esta aplicación reciba datos de Bluetooth de forma fiable mientras la aplicación está en segundo plano.</string>
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
<string name="permission_system_alert_window_label">Ventana de alerta del sistema</string>
<string name="permission_system_alert_window_description">Permitir que CAPod se dibuje sobre otras aplicaciones para hacer posible la función \"Mostrar PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Bajo consumo</string>
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
<string name="settings_scanner_mode_lowlatency_label">Baja latencia</string>
<string name="settings_monitor_mode_manual_label">Cuando la app está abierta</string>
<string name="settings_monitor_mode_automatic_label">Cuando el dispositivo está conectado</string>
<string name="settings_monitor_mode_always_label">Siempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Al verse</string>
<string name="settings_reaction_autoconnect_caseopen_label">Estuche abierto</string>
<string name="settings_reaction_autoconnect_inear_label">En la oreja</string>
<string name="pods_dual_left_label">Auricular izquierdo</string>
<string name="pods_dual_right_label">Auricular derecho</string>
<string name="pods_case_label">Estuche</string>
<string name="pods_case_status_open_label">Abierto</string>
<string name="pods_case_status_closed_label">Cerrado</string>
<string name="pods_connection_state_disconnected_label">No conectado a un dispositivo</string>
<string name="pods_connection_state_idle_label">Conectado a un dispositivo, pero inactivo</string>
<string name="pods_connection_state_music_label">En modo música</string>
<string name="pods_connection_state_call_label">En modo llamada</string>
<string name="pods_connection_state_ringing_label">Sonando</string>
<string name="pods_connection_state_hanging_up_label">Colgando</string>
<string name="pods_connection_state_unknown_label">Estado de conexión desconocido</string>
<string name="pods_unknown_raw_data_label">Datos crudos</string>
<string name="pods_unknown_label">Dispositivo desconocido</string>
<string name="pods_unknown_contact_dev">Este es un dispositivo desconocido, pero está usando un formato de mensaje similar. Agreguemos soporte para él, contactame :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
<string name="pods_charging_label">Cargando</string>
<string name="pods_inear_label">En la oreja</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="pods_yours">Tuyos</string>
<string name="headset_being_worn_label">En uso</string>
<string name="headset_not_being_worn_label">No en uso</string>
<string name="pods_case_unknown_state">Estado desconocido</string>
<string name="last_seen_x">Visto por última vez: %s</string>
<string name="first_seen_x">Visto por primera vez: %s</string>
<string name="permission_post_notifications_label">Mostrar notificaciones</string>
<string name="permission_post_notifications_description">Permitir que CAPod muestre notificaciones sobre tus AirPods, por ejemplo, su estado actual mientras están conectados.</string>
</resources>
@@ -36,4 +36,29 @@
<string name="pods_microphone_label">Micrófono</string>
<string name="pods_yours">Tuyos</string>
<string name="headset_being_worn_label">En uso</string>
<string name="general_value_not_available_label">N/D</string>
<string name="general_error_label">Error</string>
<string name="overview_nomaindevice_label">Ningún dispositivo principal</string>
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean tuyos. Enciende y conecta tu dispositivo o ajusta la configuración.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth desactivado</string>
<string name="overview_bluetooth_disabled_description">Bluetooth está desactivado, actívalo ;)</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
<string name="permission_system_alert_window_label">Ventana de alerta del sistema</string>
<string name="permission_system_alert_window_description">Permitir que CAPod se dibuje sobre otras aplicaciones para hacer posible la función \"Mostrar PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Bajo consumo</string>
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
<string name="settings_scanner_mode_lowlatency_label">Baja latencia</string>
<string name="settings_monitor_mode_manual_label">Cuando la app está abierta</string>
<string name="settings_monitor_mode_automatic_label">Cuando el dispositivo está conectado</string>
<string name="settings_monitor_mode_always_label">Siempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Al verse</string>
<string name="settings_reaction_autoconnect_caseopen_label">Estuche abierto</string>
<string name="settings_reaction_autoconnect_inear_label">En el oído</string>
<string name="headset_not_being_worn_label">No se está usando</string>
<string name="pods_case_unknown_state">Estado desconocido</string>
<string name="last_seen_x">Visto por última vez: %s</string>
<string name="first_seen_x">Visto por primera vez: %s</string>
<string name="permission_post_notifications_label">Mostrar notificaciones</string>
<string name="permission_post_notifications_description">Permitir que CAPod muestre notificaciones sobre tus AirPods, p. ej., su estado actual mientras están conectados.</string>
</resources>
+15 -14
View File
@@ -4,27 +4,27 @@
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">No disponible</string>
<string name="general_error_label">Fallo</string>
<string name="general_grant_permission_action">Conceder el permiso</string>
<string name="overview_nomaindevice_label">Sin dispositivo principal</string>
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean tuyos. Enciende y conecta tu dispositivo o ajusta la configuración.</string>
<string name="general_error_label">Error</string>
<string name="general_grant_permission_action">Otorgar el permiso</string>
<string name="overview_nomaindevice_label">Ningún dispositivo principal</string>
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean suyos. Enciende y conecta tu dispositivo o ajusta la configuración.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth desactivado</string>
<string name="overview_bluetooth_disabled_description">El Bluetooth está desactivado, actívalo ;)</string>
<string name="overview_bluetooth_disabled_description">Bluetooth está desactivado, actívalo ;)</string>
<string name="permission_bluetooth_connect_label">Conexión por Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicación requiere el permiso \"Conexión por Bluetooth\" para interactuar con los dispositivos emparejados e iniciar las conexiones.</string>
<string name="permission_bluetooth_connect_description">Esta aplicación necesita el permiso \"Conexión Bluetooth\" para interactuar con los dispositivos emparejados e iniciar conexiones.</string>
<string name="permission_bluetooth_scan_label">El Bluetooth está escaneando</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo del Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos, como tus AirPods.</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo por Bluetooth\" permite que esta aplicación detecte y reciba datos de Bluetooth de dispositivos cercanos, como los AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Esta aplicación requiere el permiso \"Bluetooth\" para conectarse a dispositivos bluetooth emparejados.</string>
<string name="permission_bluetooth_description">Esta aplicación requiere el permiso \"Bluetooth\" para conectarse a dispositivos emparejados de bluetooth.</string>
<string name="permission_access_fine_location_label">Acceso preciso a tu ubicación</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso \"localización precisa\" para recibir datos Bluetooth Low Energy. Tus auriculares utilizan la tecnología Bluetooth Low Energy para transmitir tu estado. Esta aplicación NO utilizará los datos Bluetooth para determinar tu ubicación.</string>
<string name="permission_background_location_label">Ubicación en segundo plano</string>
<string name="permission_background_location_description">CAPod utiliza el \"la ubicación en segundo plano\" para activar funciones como \"Mostrar ventana emergente\" y \"Conexión automática\" mientras la aplicación está cerrada. El acceso a la localización en segundo plano permite a la aplicación recibir datos de Bluetooth Low Energy mientras está en segundo plano. Esta aplicación NO utilizará los datos Bluetooth para determinar tu ubicación.</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso \"ubicación fina\" para recibir datos de Bluetooth de baja energía. Tus auriculares utilizan la tecnología Bluetooth de baja energía para transmitir su estado. Esta aplicación NO utilizará datos Bluetooth para determinar tu ubicación.</string>
<string name="permission_background_location_label">Acceso a la ubicación en segundo plano</string>
<string name="permission_background_location_description">CAPod utiliza \"acceso a la ubicación en segundo plano\" para habilitar características como \"Mostrar popup\" y \"AutoConnect\" mientras la aplicación está cerrada. El acceso a la ubicación en segundo plano permite que la aplicación reciba datos de Bluetooth de baja energía mientras está en segundo plano. Esta aplicación NO utilizará datos de Bluetooth para determinar su ubicación.</string>
<string name="permission_ignore_battery_optimizations_label">Desactivar las optimizaciones de la batería</string>
<string name="permission_ignore_battery_optimizations_description">Las optimizaciones de la batería impiden que esta aplicación reciba datos Bluetooth de forma fiable mientras está en segundo plano.</string>
<string name="permission_ignore_battery_optimizations_description">Las optimizaciones de la batería impiden que esta aplicación reciba datos Bluetooth de forma fiable mientras la aplicación está en segundo plano.</string>
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
<string name="permission_system_alert_window_label">Ventana de alerta del sistema</string>
<string name="permission_system_alert_window_description">Permite a CAPod dibujar sobre otras aplicaciones para hacer posible la función \"Mostrar una ventana emergente\".</string>
<string name="permission_system_alert_window_description">Permitir que CAPod funcione sobre otras aplicaciones para hacer posible la función \"Mostrar una ventana emergente\".</string>
<string name="settings_scanner_mode_lowpower_label">Bajo consumo</string>
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
<string name="settings_scanner_mode_lowlatency_label">Baja latencia</string>
@@ -53,8 +53,9 @@
<string name="pods_charging_label">Cargando</string>
<string name="pods_inear_label">En el oído</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="pods_yours">Tuyos</string>
<string name="pods_yours">Tus</string>
<string name="headset_being_worn_label">Utilizándose</string>
<string name="headset_not_being_worn_label">No se usan</string>
<string name="pods_case_unknown_state">Estado desconocido</string>
<string name="last_seen_x">Visto por última vez: %s</string>
<string name="first_seen_x">Visto por primera vez: %s</string>
@@ -2,10 +2,63 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">Tasuline CAPod</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="app_name_foss">CAPod tasuta ja vabavaraline</string>
<string name="general_value_not_available_label">Pole saadaval</string>
<string name="general_error_label">Viga</string>
<string name="general_grant_permission_action">Anna õigus</string>
<string name="overview_nomaindevice_label">Puudub peamine seade</string>
<string name="overview_nomaindevice_description">Kõik tuvastatud seadmeid ei kuulu tõenäoliselt teile. Lülitage oma seade sisse ja ühendage või muutke seadeid.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth on välja lülitatud</string>
<string name="overview_bluetooth_disabled_description">Bluetooth on keelatud, lubage see ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth ühendus</string>
<string name="permission_bluetooth_connect_description">See rakendus nõuab luba kasutada Bluetooh ühendust, et suhelda seotud seadmetega ja käivitada ühendamist.</string>
<string name="permission_bluetooth_scan_label">Bluetoothi skannimine</string>
<string name="permission_bluetooth_scan_description">Bluetoothi skannimise luba võimaldab rakendusel avastada ja vastu võtta Bluetooth andmeid lähedal asuvatest seadmetest, nagu AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">See rakendus nõuab Bluetooth luba, et ühenduda seotud Bluetooth seadmetega.</string>
<string name="permission_access_fine_location_label">Luba täpne asukoht</string>
<string name="permission_access_fine_location_description">CAPod saab täpse asukoha loaga madalama energiatarbimisega andmeid. Kõrvaklapid edastavad selle tehnoloogiaga oma olekut. See rakendus ei tuvasta Bluetooth andmetega teie asukohta.</string>
<string name="permission_background_location_label">Juurdepääs asukohale taustal</string>
<string name="permission_background_location_description">CAPod võimaldab sellega hüpikakende näitamist ja automaatset ühendumist, kui rakendus on suletud ning saada Bluetooth madala energiatarbimisega andmeid, kui rakendus töötab taustal. See rakendus ei tuvasta Bluetooth andmetega teie asukohta.</string>
<string name="permission_ignore_battery_optimizations_label">Keela aku optimeerimine</string>
<string name="permission_ignore_battery_optimizations_description">Aku optimeerimine takistab rakendusel Bluetooth andmete saamist taustal töötades.</string>
<string name="permission_required_title">Vaja on järgmist luba:</string>
<string name="permission_system_alert_window_label">Süsteemi teate aken</string>
<string name="permission_system_alert_window_description">Luba CAPodil toimetada teiste rakenduste kohal, et näha hüpikaknaid.</string>
<string name="settings_scanner_mode_lowpower_label">Madal võimsus</string>
<string name="settings_scanner_mode_balanced_label">Tasakaalustatud</string>
<string name="settings_scanner_mode_lowlatency_label">Madal latents</string>
<string name="settings_monitor_mode_manual_label">Kui rakendus on avatud</string>
<string name="settings_monitor_mode_automatic_label">Kui seade on ühendatud</string>
<string name="settings_monitor_mode_always_label">Alati</string>
<string name="settings_reaction_autoconnect_whenseen_label">Kui nähtaval</string>
<string name="settings_reaction_autoconnect_caseopen_label">Kaas on avatud</string>
<string name="settings_reaction_autoconnect_inear_label">Kõrvas</string>
<string name="pods_dual_left_label">Vasak klapp</string>
<string name="pods_dual_right_label">Parem klapp</string>
<string name="pods_case_label">Kaas</string>
<string name="pods_case_status_open_label">Avatud</string>
<string name="pods_case_status_closed_label">Suletud</string>
<string name="pods_connection_state_disconnected_label">Puudub ühendus seadmega</string>
<string name="pods_connection_state_idle_label">Ühendatud seadmega, ent ooterežiimil</string>
<string name="pods_connection_state_music_label">Muusikarežiimis</string>
<string name="pods_connection_state_call_label">Kõnerežiimis</string>
<string name="pods_connection_state_ringing_label">Heliseb</string>
<string name="pods_connection_state_hanging_up_label">Kõne lõpetamine</string>
<string name="pods_connection_state_unknown_label">Tundmatu ühenduse olek</string>
<string name="pods_unknown_raw_data_label">Algandmed</string>
<string name="pods_unknown_label">Tundmatu seade</string>
<string name="pods_unknown_contact_dev">See on tundmatu seade, mis kasutab sarnast teatevormingut. Las ma muudan selle ühilduvaks, selleks võta minuga ühendust :)</string>
<string name="pods_none_label_short">Puudub seade</string>
<string name="pods_charging_label">Laeb</string>
<string name="pods_inear_label">Kõrvas</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Sinu</string>
<string name="headset_being_worn_label">Kasutusel</string>
<string name="headset_not_being_worn_label">Ei kasuta</string>
<string name="pods_case_unknown_state">Tundmatu olek</string>
<string name="last_seen_x">Viimati kasutusel: %s</string>
<string name="first_seen_x">Esimesena kasutusel: %s</string>
<string name="permission_post_notifications_label">Näita teateid</string>
<string name="permission_post_notifications_description">"Luba CAPodil näidata AirPodsi teateid, nt praegust olekut ühendatud olles."</string>
</resources>
@@ -1,2 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">E/E</string>
<string name="general_error_label">Errorea</string>
<string name="general_grant_permission_action">Eman baimena</string>
<string name="overview_nomaindevice_label">Ez dago gailu nagusirik</string>
<string name="overview_nomaindevice_description">Detektatutako gailu guztiak nekez izango dira zureak. Piztu eta konektatu zure gailua edo egokitu ezarpenak.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth desgaituta dago</string>
<string name="overview_bluetooth_disabled_description">Bluetooth desgaituta dago, gaitu ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth konexioa</string>
<string name="permission_bluetooth_connect_description">Aplikazio honek \"Bluetooth konexioa\" baimena behar du parekatutako gailuekin elkarreragiteko eta konexioak hasteko.</string>
<string name="permission_bluetooth_scan_label">Bluetooth eskaneatzea</string>
<string name="permission_bluetooth_scan_description">\"Bluetooth eskaneatzea\" baimenak aplikazio honi zure AirPods bezalako inguruko gailuetatik Bluetooth datuak aurkitu eta jasotzeko aukera ematen dio.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Aplikazio honek \"Bluetooth\" baimena behar du parekatutako Bluetooth gailuetara konektatzeko.</string>
<string name="permission_access_fine_location_label">Sarbide zehatza kokapenera</string>
<string name="permission_access_fine_location_description">CAPod-ek \"kokapen zehatza\" baimena erabiltzen du Bluetooth Energia Baxuko datuak jasotzeko. Zure entzungailuek Bluetooth Energia Baxuko teknologia erabiltzen dute beren egoera igortzeko. Aplikazio honek EZ du Bluetooth datuak erabiliko zure kokapena zehazteko.</string>
<string name="permission_background_location_label">Atzeko planoko kokapen sarbidea</string>
<string name="permission_background_location_description">CAPod-ek \"atzeko planoko kokapen sarbidea\" erabiltzen du aplikazioa itxita dagoen bitartean \"Erakutsi pop-up\" eta \"Automatikoki konektatu\" bezalako funtzioak gaitzeko. Atzeko planoko kokapen sarbideak aplikazioari Bluetooth Energia Baxuko datuak jasotzeko aukera ematen dio atzeko planoan dagoen bitartean. Aplikazio honek EZ du Bluetooth datuak erabiliko zure kokapena zehazteko.</string>
<string name="permission_ignore_battery_optimizations_label">Desgaitu bateriaren optimizazioak</string>
<string name="permission_ignore_battery_optimizations_description">Bateriaren optimizazioek aplikazio honek atzeko planoan dagoen bitartean Bluetooth datuak modu fidagarrian jasotzea eragozten dute.</string>
<string name="permission_required_title">Baimen hau beharrezkoa da:</string>
<string name="permission_system_alert_window_label">Sistemaren Alerta Leihoa</string>
<string name="permission_system_alert_window_description">Baimendu CAPod-i beste aplikazio batzuen gainean marrazteko \"Erakutsi PopUp\" funtzioa posible egiteko.</string>
<string name="settings_scanner_mode_lowpower_label">Energia baxua</string>
<string name="settings_scanner_mode_balanced_label">Orekatua</string>
<string name="settings_scanner_mode_lowlatency_label">Latentzia baxua</string>
<string name="settings_monitor_mode_manual_label">Aplikazioa irekita dagoenean</string>
<string name="settings_monitor_mode_automatic_label">Gailua konektatuta dagoenean</string>
<string name="settings_monitor_mode_always_label">Beti</string>
<string name="settings_reaction_autoconnect_whenseen_label">Ikustean</string>
<string name="settings_reaction_autoconnect_caseopen_label">Kaxa irekita dago</string>
<string name="settings_reaction_autoconnect_inear_label">Belarrian</string>
<string name="pods_dual_left_label">Ezkerreko poda</string>
<string name="pods_dual_right_label">Eskuineko poda</string>
<string name="pods_case_label">Kaxa</string>
<string name="pods_case_status_open_label">Irekia</string>
<string name="pods_case_status_closed_label">Itxita</string>
<string name="pods_connection_state_disconnected_label">Ez dago gailu batera konektatuta</string>
<string name="pods_connection_state_idle_label">Gailu batera konektatuta, baina inaktibo</string>
<string name="pods_connection_state_music_label">Musika moduan</string>
<string name="pods_connection_state_call_label">Dei moduan</string>
<string name="pods_connection_state_ringing_label">Deitzen ari da</string>
<string name="pods_connection_state_hanging_up_label">Eskegitzen ari da</string>
<string name="pods_connection_state_unknown_label">Konexio egoera ezezaguna</string>
<string name="pods_unknown_raw_data_label">Datu gordinak</string>
<string name="pods_unknown_label">Gailu ezezaguna</string>
<string name="pods_unknown_contact_dev">Hau gailu ezezagun bat da, baina antzeko mezu formatua erabiltzen ari da. Gehitu diezaiogun euskarria, jarri nirekin harremanetan :)</string>
<string name="pods_none_label_short">Gailurik ez</string>
<string name="pods_charging_label">Kargatzen</string>
<string name="pods_inear_label">Belarrian</string>
<string name="pods_microphone_label">Mikrofonoa</string>
<string name="pods_yours">Zureak</string>
<string name="headset_being_worn_label">Jantzita</string>
<string name="headset_not_being_worn_label">Ez dago jantzita</string>
<string name="pods_case_unknown_state">Egoera ezezaguna</string>
<string name="last_seen_x">Azken aldiz ikusia: %s</string>
<string name="first_seen_x">Lehen aldiz ikusia: %s</string>
<string name="permission_post_notifications_label">Erakutsi jakinarazpenak</string>
<string name="permission_post_notifications_description">Baimendu CAPod-i zure AirPods-ei buruzko jakinarazpenak erakustea, adibidez, konektatuta dauden bitartean duten uneko egoera.</string>
</resources>
+63 -1
View File
@@ -1,2 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">موجود نیست</string>
<string name="general_error_label">خطا</string>
<string name="general_grant_permission_action">دادن دسترسی</string>
<string name="overview_nomaindevice_label">دستگاه اصلی وجود ندارد</string>
<string name="overview_nomaindevice_description">همه دستگاه‌های شناسایی شده به احتمال زیاد متعلق به شما نیستند. دستگاه خود را روشن کنید و متصل کنید یا تنظیمات را تغییر دهید.</string>
<string name="overview_bluetooth_disabled_label">بلوتوث غیرفعال است.</string>
<string name="overview_bluetooth_disabled_description">بلوتوث غیرفعال است، آن را فعال کنید. ;)</string>
<string name="permission_bluetooth_connect_label">اتصال بلوتوث</string>
<string name="permission_bluetooth_connect_description">این برنامه برای تعامل با دستگاه‌های جفت‌شده و آغاز اتصالات به مجوز «اتصال بلوتوث» نیاز دارد.</string>
<string name="permission_bluetooth_scan_label">اسکن کردن بلوتوث</string>
<string name="permission_bluetooth_scan_description">مجوز «اسکن کردن بلوتوث» به این برنامه اجازه می‌دهد داده‌های بلوتوث را از دستگاه‌های نزدیک مانند AirPods شما کشف و دریافت کند.</string>
<string name="permission_bluetooth_label">بلوتوث</string>
<string name="permission_bluetooth_description">این برنامه برای اتصال به دستگاه‌های بلوتوث جفت‌شده به مجوز «بلوتوث» نیاز دارد.</string>
<string name="permission_access_fine_location_label">دسترسی به مکان دقیق</string>
<string name="permission_access_fine_location_description">CAPod از مجوز «مکان دقیق» برای دریافت داده‌های بلوتوث کم‌مصرف استفاده می‌کند. هدفون‌های شما از فناوری بلوتوث کم‌مصرف برای پخش وضعیت خود استفاده می‌کنند. این برنامه از داده‌های بلوتوث برای تعیین موقعیت مکانی شما استفاده نخواهد کرد.</string>
<string name="permission_background_location_label">دسترسی به مکان در پس‌زمینه</string>
<string name="permission_background_location_description"> برنامه CAPod از \"دسترسی به مکان در پس‌زمینه\" برای فعال کردن ویژگی‌هایی مانند \"نمایش پنجره بازشو\" و \"اتصال خودکار\" در زمانی که برنامه بسته است، استفاده می‌کند. این دسترسی به برنامه امکان دریافت داده‌های بلوتوث کم مصرف در پس‌زمینه را می‌دهد. این برنامه از داده‌های بلوتوث برای تعیین موقعیت مکانی شما استفاده نخواهد کرد.</string>
<string name="permission_ignore_battery_optimizations_label">غیرفعال کردن بهینه‌سازی‌ باتری</string>
<string name="permission_ignore_battery_optimizations_description">بهینه‌سازی‌ باتری باعث می‌شود این برنامه نتواند به‌طور قابل‌اعتماد داده‌های بلوتوث را هنگامی که در پس‌زمینه فعال است، دریافت کند.</string>
<string name="permission_required_title">مجوز زیر مورد نیاز است:</string>
<string name="permission_system_alert_window_label">پنجره هشدار سیستمی</string>
<string name="permission_system_alert_window_description">اجازه دهید CAPod بتواند روی برنامه‌های دیگر نمایش داده شود تا قابلیت \"نمایش پنجره بازشو\" امکان‌پذیر شود.</string>
<string name="settings_scanner_mode_lowpower_label">قدرت کم</string>
<string name="settings_scanner_mode_balanced_label">متعادل</string>
<string name="settings_scanner_mode_lowlatency_label">تأخیر کم</string>
<string name="settings_monitor_mode_manual_label">وقتی برنامه باز است</string>
<string name="settings_monitor_mode_automatic_label">وقتی دستگاه وصل است</string>
<string name="settings_monitor_mode_always_label">همیشه</string>
<string name="settings_reaction_autoconnect_whenseen_label">موقع دیدن</string>
<string name="settings_reaction_autoconnect_caseopen_label">بسته بودن کیس</string>
<string name="settings_reaction_autoconnect_inear_label">در گوش</string>
<string name="pods_dual_left_label">گوش چپ</string>
<string name="pods_dual_right_label">گوش راست</string>
<string name="pods_case_label">کیس</string>
<string name="pods_case_status_open_label">باز</string>
<string name="pods_case_status_closed_label">بسته</string>
<string name="pods_connection_state_disconnected_label">هیچ دستگاهی متصل نیست</string>
<string name="pods_connection_state_idle_label">متصل به یک دستگاه، اما غیرفعال</string>
<string name="pods_connection_state_music_label">در حالت موسیقی</string>
<string name="pods_connection_state_call_label">در حالت تماس</string>
<string name="pods_connection_state_ringing_label">زنگ زدن</string>
<string name="pods_connection_state_hanging_up_label">قطع کردن</string>
<string name="pods_connection_state_unknown_label">وضعیت اتصال نامشخص</string>
<string name="pods_unknown_raw_data_label">داده های خام</string>
<string name="pods_unknown_label">دستگاه ناشناخته</string>
<string name="pods_unknown_contact_dev">این یک دستگاه ناشناخته است، اما از فرمت پیام مشابهی استفاده می‌کند. بیایید از آن پشتیبانی کنیم، با من تماس بگیرید. :)</string>
<string name="pods_none_label_short">بدون دستگاه</string>
<string name="pods_charging_label">درحال شارژ</string>
<string name="pods_inear_label">در گوش</string>
<string name="pods_microphone_label">میکروفون</string>
<string name="pods_yours">مال شما</string>
<string name="headset_being_worn_label">در حال استفاده</string>
<string name="headset_not_being_worn_label">عدم استفاده</string>
<string name="pods_case_unknown_state">وضعیت ناشناخته</string>
<string name="last_seen_x">آخرین بازدید: %s</string>
<string name="first_seen_x">اولین بازدید: %s</string>
<string name="permission_post_notifications_label">نمایش اعلان‌ها</string>
<string name="permission_post_notifications_description">"اجازه دهید CAPod اعلان‌هایی درباره ایرپادهای شما نمایش دهد، مانند وضعیت فعلی آنها هنگام اتصال."</string>
</resources>
+71 -1
View File
@@ -1,2 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N/A</string>
<string name="general_error_label">Virhe</string>
<string name="general_grant_permission_action">Myönnä lupa</string>
<string name="overview_nomaindevice_label">Ei ensisijaista laitetta</string>
<string name="overview_nomaindevice_description">Kaikki havaitut laitteet eivät todennäköisesti ole sinun. Kytke laitteeseesi virta ja yhdistä se tai säädä asetuksia.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth on pois käytöstä</string>
<string name="overview_bluetooth_disabled_description">Bluetooth on pois käytöstä, ota se käyttöön ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth-yhteys</string>
<string name="permission_bluetooth_connect_description">Tämä sovellus vaatii \"Bluetooth-yhteys\"-luvan ollakseen vuorovaikutuksessa pariliitettyjen laitteiden kanssa ja aloittaakseen yhteyksiä.</string>
<string name="permission_bluetooth_scan_label">Bluetooth-skannaus</string>
<string name="permission_bluetooth_scan_description">\"Bluetooth-skannaus\"-lupa antaa tämän sovelluksen löytää ja vastaanottaa Bluetooth-tietoja lähellä olevista laitteista, kuten AirPodeistasi.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Tämä sovellus vaatii \"Bluetooth\"-luvan yhdistääkseen pariliitettyihin Bluetooth-laitteisiin.</string>
<string name="permission_access_fine_location_label">Tarkka sijainti</string>
<string name="permission_access_fine_location_description">CAPod käyttää \"tarkka sijainti\" -lupaa vastaanottaakseen Bluetooth Low Energy -tietoja. Kuulokkeesi käyttävät Bluetooth Low Energy -tekniikkaa lähettääkseen tilansa. Tämä sovellus EI käytä Bluetooth-tietoja sijaintisi määrittämiseen.</string>
<string name="permission_background_location_label">Sijainnin käyttö taustalla</string>
<string name="permission_background_location_description">CAPod käyttää \"sijainnin käyttö taustalla\" -lupaa mahdollistaakseen ominaisuuksia, kuten \"Näytä ponnahdusikkuna\" ja \"Automaattinen yhdistäminen\", kun sovellus on suljettu. Sijainnin käyttö taustalla antaa sovelluksen vastaanottaa Bluetooth Low Energy -tietoja sen ollessa taustalla. Tämä sovellus EI käytä Bluetooth-tietoja sijaintisi määrittämiseen.</string>
<string name="permission_ignore_battery_optimizations_label">Poista akun optimoinnit käytöstä</string>
<string name="permission_ignore_battery_optimizations_description">Akun optimoinnit estävät tätä sovellusta vastaanottamasta luotettavasti Bluetooth-tietoja sovelluksen ollessa taustalla.</string>
<string name="permission_required_title">Seuraava lupa vaaditaan:</string>
<string name="permission_system_alert_window_label">Järjestelmän ilmoitusikkuna</string>
<string name="permission_system_alert_window_description">Salli CAPodin piirtää muiden sovellusten päälle mahdollistaaksesi \"Näytä ponnahdusikkuna\" -ominaisuuden.</string>
<string name="settings_scanner_mode_lowpower_label">Alhainen virrankulutus</string>
<string name="settings_scanner_mode_balanced_label">Tasapainotettu</string>
<string name="settings_scanner_mode_lowlatency_label">Matala viive</string>
<string name="settings_monitor_mode_manual_label">Kun sovellus on auki</string>
<string name="settings_monitor_mode_automatic_label">Kun laite on yhdistetty</string>
<string name="settings_monitor_mode_always_label">Aina</string>
<string name="settings_reaction_autoconnect_whenseen_label">Kun havaittu</string>
<string name="settings_reaction_autoconnect_caseopen_label">Kotelo on auki</string>
<string name="settings_reaction_autoconnect_inear_label">Korvassa</string>
<string name="pods_dual_left_label">Vasen kuuloke</string>
<string name="pods_dual_right_label">Oikea kuuloke</string>
<string name="pods_case_label">Kotelo</string>
<string name="pods_case_status_open_label">Auki</string>
<string name="pods_case_status_closed_label">Suljettu</string>
<string name="pods_connection_state_disconnected_label">Ei yhteydessä laitteeseen</string>
<string name="pods_connection_state_idle_label">Yhdistetty laitteeseen, mutta käyttämättömänä</string>
<string name="pods_connection_state_music_label">Musiikkitilassa</string>
<string name="pods_connection_state_call_label">Puhelutilassa</string>
<string name="pods_connection_state_ringing_label">Soi</string>
<string name="pods_connection_state_hanging_up_label">Lopettaa puhelua</string>
<string name="pods_connection_state_unknown_label">Tuntematon yhteystila</string>
<string name="pods_unknown_raw_data_label">Raakadata</string>
<string name="pods_unknown_label">Tuntematon laite</string>
<string name="pods_unknown_contact_dev">Tämä on tuntematon laite, mutta se käyttää samanlaista viestimuotoa. Lisätään tuki sille, ota yhteyttä :)</string>
<string name="pods_none_label_short">Ei laitetta</string>
<string name="pods_charging_label">Latautuu</string>
<string name="pods_inear_label">Korvassa</string>
<string name="pods_microphone_label">Mikrofoni</string>
<string name="pods_yours">Sinun</string>
<string name="headset_being_worn_label">Käytössä</string>
<string name="headset_not_being_worn_label">Ei käytössä</string>
<string name="pods_case_unknown_state">Tuntematon tila</string>
<string name="last_seen_x">Viimeksi nähty: %s</string>
<string name="first_seen_x">Ensimmäisen kerran nähty: %s</string>
<string name="permission_post_notifications_label">Näytä ilmoitukset</string>
<string name="permission_post_notifications_description">"Salli CAPodin näyttää ilmoituksia AirPodeistasi, esim. niiden nykyinen tila yhdistettynä."</string>
</resources>
+65 -11
View File
@@ -1,18 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Hindi Magagamit</string>
<string name="general_error_label">Error</string>
<string name="general_grant_permission_action">Bigyan ng permiso</string>
<string name="overview_nomaindevice_description">Lahat ng mga device ay malabong saiyo. I-power on at i-connect ang device mo o i-adjust ang settings.</string>
<string name="permission_bluetooth_connect_description">Ang app na ito ay kailangan ng \"Bluetooth connect\" permission para makipag interact sa mga na-paired na devices at maka gawa ng connection.</string>
<string name="permission_bluetooth_scan_description">Ang \"Bluetooth scanning\" permission ay pag na ibigay, itong app na ito ay puwedeng mag diskubre o maka receive ng data galing sa isang malapit na Bluetooth devices tulad ng iyong AirPods.</string>
<string name="permission_bluetooth_description">Ang app na ito ay kailangan ng \"Bluetooth\" permission upang maka connect sa naka-paired na bluetooth device.</string>
<string name="permission_access_fine_location_description">Ang CAPod app ay gumagamit ng \"Fine location\" permission para maka receiveng Bluetooth Low Energy Data. Iyong headphones ay gumagamit ng Bluetooth Low Energy technology pa ma-broadcaast ang kanilang Status. Itong app na ito ay HINDI gagamitin ang Bluetooth data para malaman ang iyong lokasyon.</string>
<string name="overview_nomaindevice_label">Walang pangunahing device</string>
<string name="overview_nomaindevice_description">Lahat ng mga natukoyang device ay malamang na hindi sa iyo. I-on at ikonekta ang iyong device o ayusin ang mga setting.</string>
<string name="overview_bluetooth_disabled_label">Naka-disable ang Bluetooth</string>
<string name="overview_bluetooth_disabled_description">Naka-disable ang Bluetooth, i-enable ito ;)</string>
<string name="permission_bluetooth_connect_label">Kumonekta sa Bluetooth</string>
<string name="permission_bluetooth_connect_description">Ang app na ito ay nangangailangan ng pahintulot na \"Kumonekta sa Bluetooth\" upang makipag-ugnayan sa mga naka-pares na device at simulan ang mga koneksyon.</string>
<string name="permission_bluetooth_scan_label">Pag-scan sa Bluetooth</string>
<string name="permission_bluetooth_scan_description">Ang pahintulot na \"Pag-scan sa Bluetooth\" ay nagbibigay-daan sa app na ito na matuklasan at makatanggap ng data ng Bluetooth mula sa mga kalapit na device tulad ng iyong AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Ang app na ito ay nangangailangan ng pahintulot na \"Bluetooth\" upang kumonekta sa mga naka-pares na bluetooth device.</string>
<string name="permission_access_fine_location_label">I-access ang eksaktong lokasyon</string>
<string name="permission_access_fine_location_description">Ginagamit ng CAPod ang pahintulot na \"eksaktong lokasyon\" upang makatanggap ng data ng Bluetooth Low Energy. Ginagamit ng iyong mga headphone ang teknolohiyang Bluetooth Low Energy upang i-broadcast ang kanilang status. HINDI gagamitin ng app na ito ang data ng Bluetooth upang matukoy ang iyong lokasyon.</string>
<string name="permission_background_location_label">Access sa lokasyon sa background</string>
<string name="permission_background_location_description">Ginagamit ng CAPod ang \"access sa lokasyon sa background\" upang paganahin ang mga feature tulad ng \"Ipakita ang popup\" at \"AutoConnect\" habang sarado ang app. Ang access sa lokasyon sa background ay nagbibigay-daan sa app na makatanggap ng data ng Bluetooth Low Energy habang nasa background ito. HINDI gagamitin ng app na ito ang data ng Bluetooth upang matukoy ang iyong lokasyon.</string>
<string name="permission_ignore_battery_optimizations_label">I-disable ang mga pag-optimize ng baterya</string>
<string name="permission_ignore_battery_optimizations_description">Pinipigilan ng mga pag-optimize ng baterya ang app na ito na mapagkakatiwalaang makatanggap ng data ng Bluetooth habang nasa background ang app.</string>
<string name="permission_required_title">Kinakailangan ang sumusunod na pahintulot:</string>
<string name="permission_system_alert_window_label">System Alert Window</string>
<string name="permission_system_alert_window_description">Payagan ang CAPod na mag-drawing sa ibabaw ng iba pang mga app upang maging posible ang feature na \"Ipakita ang PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Mababang konsumo ng kuryente</string>
<string name="settings_scanner_mode_balanced_label">Balanse</string>
<string name="settings_scanner_mode_lowlatency_label">Mababang latency</string>
<string name="settings_monitor_mode_manual_label">Kapag bukas ang app</string>
<string name="settings_monitor_mode_automatic_label">Kapag konektado ang device</string>
<string name="settings_monitor_mode_always_label">Palagi</string>
<string name="settings_reaction_autoconnect_whenseen_label">Kapag nakita</string>
<string name="settings_reaction_autoconnect_caseopen_label">Bukas ang case</string>
<string name="settings_reaction_autoconnect_inear_label">Nasa tainga</string>
<string name="pods_dual_left_label">Kaliwang pod</string>
<string name="pods_dual_right_label">Kanan na pod</string>
<string name="pods_dual_right_label">Kanang pod</string>
<string name="pods_case_label">Case</string>
<string name="pods_case_status_open_label">Buksan</string>
<string name="pods_case_status_closed_label">Sarado</string>
<string name="pods_connection_state_disconnected_label">Hindi Konektado sa device</string>
<string name="pods_unknown_contact_dev">Ito ay hindi kilalang Device, pero ginagamit ang kilalang message format. lagyan nating ng support para diyan, contact mo ko :)</string>
<string name="pods_yours">Sayo</string>
<string name="headset_being_worn_label">Ginagamit</string>
<string name="pods_case_unknown_state">Hindi alam ang stado</string>
<string name="pods_connection_state_disconnected_label">Hindi konektado sa isang device</string>
<string name="pods_connection_state_idle_label">Konektado sa isang device, ngunit idle</string>
<string name="pods_connection_state_music_label">Nasa music mode</string>
<string name="pods_connection_state_call_label">Nasa call mode</string>
<string name="pods_connection_state_ringing_label">Nagri-ring</string>
<string name="pods_connection_state_hanging_up_label">Binababa ang tawag</string>
<string name="pods_connection_state_unknown_label">Hindi kilalang estado ng koneksyon</string>
<string name="pods_unknown_raw_data_label">Raw data</string>
<string name="pods_unknown_label">Hindi kilalang device</string>
<string name="pods_unknown_contact_dev">Ito ay isang hindi kilalang device, ngunit gumagamit ito ng katulad na format ng mensahe. Suportahan natin ito, makipag-ugnayan sa akin :)</string>
<string name="pods_none_label_short">Walang device</string>
<string name="pods_charging_label">Nagcha-charge</string>
<string name="pods_inear_label">Nasa tainga</string>
<string name="pods_microphone_label">Mikropono</string>
<string name="pods_yours">Sa iyo</string>
<string name="headset_being_worn_label">Suot</string>
<string name="headset_not_being_worn_label">Hindi suot</string>
<string name="pods_case_unknown_state">Hindi kilalang estado</string>
<string name="last_seen_x">Huling nakita: %s</string>
<string name="first_seen_x">Unang nakita: %s</string>
<string name="permission_post_notifications_label">Ipakita ang mga notification</string>
<string name="permission_post_notifications_description">"Payagan ang CAPod na magpakita ng mga notification tungkol sa iyong AirPods, hal. ang kanilang kasalukuyang status habang konektado."</string>
</resources>
@@ -3,7 +3,7 @@
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS (libre)</string>
<string name="general_value_not_available_label">Non disponible</string>
<string name="general_value_not_available_label">N.D.</string>
<string name="general_error_label">Erreur</string>
<string name="general_grant_permission_action">Accorder lautorisation</string>
<string name="overview_nomaindevice_label">Aucun appareil principal</string>
@@ -19,12 +19,12 @@
<string name="permission_access_fine_location_label">Accéder à la position précise</string>
<string name="permission_access_fine_location_description">CAPod utilise lautorisation « position précise » pour recevoir les données « faible énergie » de Bluetooth. Votre casque-micro utilise la technologie « basse énergie » de Bluetooth pour diffuser son état. Cette appli nutilisera PAS les données Bluetooth pour déterminer votre position.</string>
<string name="permission_background_location_label">Accès à la position en arrière-plan</string>
<string name="permission_background_location_description">CAPod utilise « laccès à la position en arrière-plan » afin de permettre des fonctions telles qu’« Afficher une fenêtre surgissante » et « Connexion automatique » alors que lappli est fermée. Laccès à la position en arrière-plan permet à lappli de recevoir les données « basse énergie » de Bluetooth. Elles ne seront PAS utilisées pour déterminer votre position géographie.</string>
<string name="permission_ignore_battery_optimizations_label">Désactiver les optimisations de la pile/batterie</string>
<string name="permission_ignore_battery_optimizations_description">Les optimisations de la pile/batterie empêchent lappli de recevoir les données Bluetooth de manière fiable alors que lappli est en arrière-plan.</string>
<string name="permission_background_location_description">CAPod utilise « laccès à la position en arrière-plan » afin de permettre des fonctions telles qu’« Afficher une fenêtre contextuelle » et « Connexion automatique » alors que lappli est fermée. Laccès à la position en arrière-plan permet à lappli de recevoir les données « basse énergie » de Bluetooth. Elles ne seront PAS utilisées pour déterminer votre position géographie.</string>
<string name="permission_ignore_battery_optimizations_label">Désactiver les optimisations de la batterie</string>
<string name="permission_ignore_battery_optimizations_description">Les optimisations de la batterie empêchent lappli de recevoir les données Bluetooth de manière fiable alors que lappli est en arrière-plan.</string>
<string name="permission_required_title">Lautorisation suivante est nécessaire :</string>
<string name="permission_system_alert_window_label">Fenêtre dalerte système</string>
<string name="permission_system_alert_window_description">Permettre à CAPod dapparaître par-dessus les autres applis pour rendre la fonction « Afficher une fenêtre surgissante » possible.</string>
<string name="permission_system_alert_window_description">Permettre à CAPod dapparaître par-dessus les autres applis pour rendre la fonction « Afficher une fenêtre contextuelle » possible.</string>
<string name="settings_scanner_mode_lowpower_label">Faible puissance</string>
<string name="settings_scanner_mode_balanced_label">Équilibré</string>
<string name="settings_scanner_mode_lowlatency_label">Faible latence</string>
@@ -55,6 +55,7 @@
<string name="pods_microphone_label">Microphone</string>
<string name="pods_yours">Les vôtres</string>
<string name="headset_being_worn_label">Porté</string>
<string name="headset_not_being_worn_label">Non porté</string>
<string name="pods_case_unknown_state">État inconnu</string>
<string name="last_seen_x">Vu pour la dernière fois : %s</string>
<string name="first_seen_x">Vu pour la première fois : %s</string>
@@ -1,2 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N/D</string>
<string name="general_error_label">Erro</string>
<string name="general_grant_permission_action">Conceder permiso</string>
<string name="overview_nomaindevice_label">Sen dispositivo principal</string>
<string name="overview_nomaindevice_description">É improbable que todos os dispositivos detectados sexan teus. Acende e conecta o teu dispositivo ou axusta a configuración.</string>
<string name="overview_bluetooth_disabled_label">O Bluetooth está desactivado</string>
<string name="overview_bluetooth_disabled_description">O Bluetooth está desactivado, actívao ;)</string>
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicación require o permiso de \"Conexión Bluetooth\" para interactuar con dispositivos emparellados e iniciar conexións.</string>
<string name="permission_bluetooth_scan_label">Escaneo Bluetooth</string>
<string name="permission_bluetooth_scan_description">O permiso de \"Escaneo Bluetooth\" permite a esta aplicación descubrir e recibir datos Bluetooth de dispositivos próximos como os teus AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Esta aplicación require o permiso de \"Bluetooth\" para conectarse a dispositivos bluetooth emparellados.</string>
<string name="permission_access_fine_location_label">Acceder á localización precisa</string>
<string name="permission_access_fine_location_description">CAPod usa o permiso de \"localización precisa\" para recibir datos de Bluetooth Low Energy. Os teus auriculares usan a tecnoloxía Bluetooth Low Energy para transmitir o seu estado. Esta aplicación NON usará datos Bluetooth para determinar a túa localización.</string>
<string name="permission_background_location_label">Acceso á localización en segundo plano</string>
<string name="permission_background_location_description">CAPod usa o \"acceso á localización en segundo plano\" para habilitar funcións como \"Mostrar ventá emerxente\" e \"Conexión automática\" mentres a aplicación está pechada. O acceso á localización en segundo plano permite á aplicación recibir datos de Bluetooth Low Energy mentres está en segundo plano. Esta aplicación NON usará datos Bluetooth para determinar a túa localización.</string>
<string name="permission_ignore_battery_optimizations_label">Desactivar optimizacións da batería</string>
<string name="permission_ignore_battery_optimizations_description">As optimizacións da batería impiden que esta aplicación reciba datos Bluetooth de forma fiable mentres a aplicación está en segundo plano.</string>
<string name="permission_required_title">Requírese o seguinte permiso:</string>
<string name="permission_system_alert_window_label">Ventá de alerta do sistema</string>
<string name="permission_system_alert_window_description">Permitir a CAPod debuxar sobre outras aplicacións para facer posible a función \"Mostrar ventá emerxente\".</string>
<string name="settings_scanner_mode_lowpower_label">Baixa potencia</string>
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
<string name="settings_scanner_mode_lowlatency_label">Baixa latencia</string>
<string name="settings_monitor_mode_manual_label">Cando a aplicación está aberta</string>
<string name="settings_monitor_mode_automatic_label">Cando o dispositivo está conectado</string>
<string name="settings_monitor_mode_always_label">Sempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Cando se ve</string>
<string name="settings_reaction_autoconnect_caseopen_label">A funda está aberta</string>
<string name="settings_reaction_autoconnect_inear_label">Na orella</string>
<string name="pods_dual_left_label">Auricular esquerdo</string>
<string name="pods_dual_right_label">Auricular dereito</string>
<string name="pods_case_label">Funda</string>
<string name="pods_case_status_open_label">Aberta</string>
<string name="pods_case_status_closed_label">Pechada</string>
<string name="pods_connection_state_disconnected_label">Non conectado a un dispositivo</string>
<string name="pods_connection_state_idle_label">Conectado a un dispositivo, pero inactivo</string>
<string name="pods_connection_state_music_label">En modo música</string>
<string name="pods_connection_state_call_label">En modo chamada</string>
<string name="pods_connection_state_ringing_label">Soando</string>
<string name="pods_connection_state_hanging_up_label">Colgando</string>
<string name="pods_connection_state_unknown_label">Estado de conexión descoñecido</string>
<string name="pods_unknown_raw_data_label">Datos brutos</string>
<string name="pods_unknown_label">Dispositivo descoñecido</string>
<string name="pods_unknown_contact_dev">Este é un dispositivo descoñecido, pero está a usar un formato de mensaxe similar. Imos engadir soporte para el, ponte en contacto comigo :)</string>
<string name="pods_none_label_short">Sen dispositivo</string>
<string name="pods_charging_label">Cargando</string>
<string name="pods_inear_label">Na orella</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="pods_yours">Teus</string>
<string name="headset_being_worn_label">Postos</string>
<string name="headset_not_being_worn_label">Non postos</string>
<string name="pods_case_unknown_state">Estado descoñecido</string>
<string name="last_seen_x">Visto por última vez: %s</string>
<string name="first_seen_x">Visto por primeira vez: %s</string>
<string name="permission_post_notifications_label">Mostrar notificacións</string>
<string name="permission_post_notifications_description">"Permitir a CAPod mostrar notificacións sobre os teus AirPods, p. ex., o seu estado actual mentres están conectados."</string>
</resources>
@@ -2,8 +2,8 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">कैपोड</string>
<string name="app_name_pro">कैपोड प्रो</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N/A</string>
<string name="app_name_foss">कैपॉड फॉस्स</string>
<string name="general_value_not_available_label">अनुवाद उपलब्ध नहीं है।</string>
<string name="general_error_label">त्रुटि</string>
<string name="general_grant_permission_action">अनुदान अनुमति</string>
<string name="overview_nomaindevice_label">कोई प्राथमिक उपकरण नहीं</string>
@@ -55,7 +55,10 @@
<string name="pods_microphone_label">माइक्रोफ़ोन</string>
<string name="pods_yours">आपका</string>
<string name="headset_being_worn_label">पहना जा रहा है</string>
<string name="headset_not_being_worn_label">नहीं पहना गया</string>
<string name="pods_case_unknown_state">अज्ञात अवस्था</string>
<string name="last_seen_x">अंतिम बार देखे गए: %s</string>
<string name="first_seen_x">पहली बार देखा गया: %s</string>
<string name="permission_post_notifications_label">नोटिफिकेशन्स सूचनाएँ दिखाएँ</string>
<string name="permission_post_notifications_description">"CAPod को आपके AirPods के बारे में नोटिफिकेशन दिखाने दें, जैसे कि कनेक्टेड रहते हुए उनका वर्तमान स्टेटस।"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More