Compare commits

...
392 Commits
Author SHA1 Message Date
darken ba554c8787 Release: 3.0.4-rc1 2026-02-09 16:51:10 +01:00
darken 968829072a chore: Update app translations from Crowdin
Pull latest string translations from Crowdin.
Validated and reverted any problematic changes.
2026-02-09 16:48:43 +01:00
darken 0d1f331551 Release: 3.0.4-rc0 2026-02-09 16:22:31 +01:00
darken a8bb42d40b fix(ci): Narrow APK glob to avoid uploading duplicates
The copyTo-based APK renaming (from AGP 9 upgrade) leaves both the
original and renamed APK in the output directory. Narrow the glob to
only match renamed APKs.
2026-02-09 16:16:18 +01:00
darken 9a3fb11ef7 fix: Use correct labels for upgrade and donate menu items
Both menu items were using settings_general_label ("Settings") instead
of their proper labels, affecting accessibility and long-press tooltips.
2026-02-09 16:07:25 +01:00
darken 12d2c4dd06 fix: Pre-release cleanup of manifest, resource leaks, and dependencies
- Fix BleScanResultReceiver package name in manifest (.bluetooth → .common.bluetooth)
- Fix HandlerThread leak in BluetoothManager2 when registerReceiver() throws
- Cache battery values in MonitorNotifications to match hardening pattern
- Remove duplicate fragment-ktx dependency and align fragment-testing version
- Remove deprecated lifecycle-extensions dependency
2026-02-09 15:50:39 +01:00
darken cfed733fa7 fix: Guard MonitorService start against missing Bluetooth permissions 2026-02-09 14:24:45 +01:00
darken 1af8743533 chore: Remove redundant gradle.properties after AGP 9 upgrade 2026-02-09 07:50:36 +01:00
darken 2f3f1f3f70 fix(ci): Remove non-debug unit test variants for AGP 9 compatibility 2026-02-08 17:31:19 +01:00
darken 6223524a1e chore: Upgrade to AGP 9.0.0 and Gradle 9.3.1 2026-02-08 17:31:19 +01:00
darken ebfffb1536 fix: Replace WorkManager with ForegroundService to eliminate excessive wakelocks
Replace MonitorWorker (CoroutineWorker) with MonitorService (ForegroundService)
to eliminate persistent ProcessorForegroundLck wakelock reported in Android Vitals.
Add BootCompletedReceiver for post-reboot auto-start.
Remove WorkManager dependency entirely.
2026-02-08 14:19:11 +01:00
darken 5aa36b148b fix: Harden battery display against native crashes
Remove thread-unsafe shared RelativeDateTimeFormatter instance and cache
battery percentage values to local variables at all call sites.
2026-02-08 04:25:30 +01:00
darken 29391feaf8 chore: Remove stale CLAUDE.md exclusion from _config.yml 2026-02-07 20:17:51 +01:00
darken 4e279ca44d chore: Restructure Claude Code config into modular rules
Migrate monolithic root CLAUDE.md into .claude/rules/ structure
with focused topic files and glob-based contextual loading.
Clean up leftover helper scripts in .claude/tmp/.
2026-02-07 19:31:28 +01:00
darken 9ff8d9e04e fix: Handle display cutouts in landscape mode
EdgeToEdgeHelper now considers both systemBars() and displayCutout()
insets when applying padding. This prevents camera cutouts from
obscuring UI content when the device is in landscape orientation.
2026-01-17 14:20:21 +01:00
darken 10d5a823fc fix: Make widget clicks unique
Use the widget ID as the request code for `PendingIntent`. This ensures that each widget instance has a distinct `PendingIntent`, preventing them from overwriting each other and allowing clicks on multiple widgets to work correctly.
2025-11-11 08:37:37 +01:00
darken 54afadb136 fix: Prevent memory leak in WidgetConfigurationActivity
Uses the application context instead of the activity context when updating the widget. This avoids leaking the activity instance if the update operation outlives the configuration screen.
2025-11-11 08:37:37 +01:00
darken e4db9387ca Update translations 2025-11-10 08:38:12 +01:00
darken 4834eb276a Widget: Fix layout for devices with single charge detection
The instance check was being done on the wrong value, so it was never `true`.
2025-11-09 15:03:04 +01:00
darken 5860bbffb6 Release: 3.0.3-rc0 2025-10-29 15:52:54 +01:00
darken da53d68190 Update translations 2025-10-29 15:52:04 +01:00
darken b397bf190c Update translations 2025-10-29 15:52:04 +01:00
darken 15d664c361 feat: Make connectedDevices flow more robust
Adds retry and catch logic to handle failures gracefully.
2025-10-29 15:51:48 +01:00
darken d2f24d8b95 fix: Improve BluetoothManager stability
Enhances robustness by adding extensive error handling around broadcast receiver registration, profile event processing, and service disconnections to prevent crashes. Also ensures device flow is cleared when Bluetooth is disabled.
2025-10-29 15:51:48 +01:00
darken b5436659b6 refactor: Make connectedDevices a hot StateFlow
Convert `connectedDevices()` from a function returning a cold Flow to a property that is a hot `StateFlow`. This simplifies call sites and improves efficiency by sharing the underlying subscription.
2025-10-29 15:51:48 +01:00
darken 1cfc4611cb Improve Bluetooth device connection monitoring
Switched from monitoring generic ACL events to specific Headset profile state changes for more reliable device connection and disconnection detection.

This fixes a race condition where CAPod thinks no device is connected because we triggered too early, before "connectedDevices" on the HEADSET profile contains our target device.

This fixes #313
2025-10-29 15:14:45 +01:00
darken bda0a20743 Release: 3.0.2-rc0 2025-10-11 08:02:51 +02:00
darken 09bad90c0c feat: Add and update translations for Dutch and Mexican Spanish 2025-10-11 07:50:32 +02:00
darken 34856607f8 Fix crash on start due to R8
Closes #341
2025-10-11 07:45:42 +02:00
darken 5c2b66fe27 Release: 3.0.1-rc0 2025-10-10 08:35:19 +02:00
darken c9fe9a43e2 Update and extend translations
This commit updates translations for German, Spanish, and Catalan. It also adds a comprehensive set of new strings for the Chinese (Simplified) localization.
2025-10-10 08:34:29 +02:00
darken 395dbbb624 Feat: Update translations
Updates for Russian, Polish, Catalan, Spanish, and Czech.
2025-10-09 12:24:14 +02:00
darken fdc6486102 Feat: Update translations
Updates for Russian, Polish, Catalan, Spanish, and Czech.
2025-10-09 12:24:14 +02:00
darken 6c379451c0 Add ear detection limitation notice to reactions settings
Explains that single-pod detection is an Apple limitation, not an app bug.
Users experiencing this issue will now understand it only affects the
"primary pod" (microphone pod) and can be configured in iOS settings.

Closes #38, Closes #329
2025-10-08 15:37:14 +02:00
darken 221f3ccbed Refactor: Convert build configuration to Gradle plugin pattern
- Migrate ProjectConfig from static object to proper Gradle plugin
- Add version type support (beta/rc) to version.properties
- Clean up legacy fastlane changelogs
- Update release script to support version types
- Remove automatic fastlane changelog generation from release script
- Add BuildConfig field injection for version info
2025-10-07 13:54:24 +02:00
darken 12f0662146 Release: 3.0.0-beta1 2025-10-07 12:44:45 +02:00
darken 7cc81bd554 Chore: Update release tooling
Update Fastlane and simplify GitHub release workflow.
2025-10-07 12:43:50 +02:00
darken 618fddb698 Update translations 2025-10-07 12:43:33 +02:00
darken 0f74cb9b39 Refactor: Use plurals for unmatched devices count. 2025-10-07 12:43:33 +02:00
darken 743f5aeb89 Release: 3.0.0-beta0 2025-10-07 10:59:23 +02:00
darken 9260a24e0d Feat: Update translations
- Add new strings for device profiles and settings
- Improve existing translations for clarity
- Update Google Play billing error messages
2025-10-07 10:58:27 +02:00
darken 49fc0f018a Implemented widget configuration for multiple profiles 2025-09-29 19:29:43 +02:00
darken ad7089ea7c Allow key inputs with spaces instead of dashes
The regex for validating the identity key in the device profile creation fragment has been updated to also allow spaces as separators.
2025-09-29 15:50:53 +02:00
Matthias Urhahn e74b83581a Merge pull request #330 from d4rken-org/multi-device
Support for multiple device profiles
2025-09-29 15:10:18 +02:00
darken a3fb26f967 Update translations 2025-09-29 14:57:41 +02:00
darken 2b4161b9e7 Fix DeviceManagerFragment navigation
The navigation graph had a duplicate entry for DeviceManagerFragment.
This commit removes the duplicate and ensures the correct navigation action to DeviceProfileCreationFragment is present.
2025-09-29 14:31:58 +02:00
darken d38f2ea3e9 Fix: Add missing % to signal quality string.
Refactor: Simplify signal quality filter.
2025-09-29 14:30:38 +02:00
darken ba4445f19d Fix lid state for AirPods Gen 4
The lid state for AirPods Gen 4 was incorrectly reported as UNKNOWN when it should have been NOT_IN_CASE. This commit fixes the issue in both the ANC and non-ANC tests.
2025-09-29 14:23:56 +02:00
darken 33cab939f0 Refactor: Use DeviceProfilesRepo for keys
This commit replaces the direct usage of GeneralSettings for fetching identity and encryption keys with the DeviceProfilesRepo. BaseAirPodsTest is updated to reflect this change, mocking DeviceProfilesRepo instead of GeneralSettings.
2025-09-29 14:23:52 +02:00
darken 69ce7e18f5 Add "Monitoring for devices" card to Overview
This commit introduces a new card to the Overview screen that informs the user that the app is actively monitoring for devices when there are profiles configured but no devices are currently connected.
2025-09-29 14:14:09 +02:00
darken e2014a26fc Gradle update to 8.13.0 2025-09-29 14:06:34 +02:00
darken 1e806ea416 Lower default minimum signal quality 2025-09-29 14:04:14 +02:00
darken 6cf85935f9 TS: Auto create profile from closest device 2025-09-29 14:04:07 +02:00
darken 25b838da5c Rebase regression 2025-09-29 14:02:16 +02:00
darken 03676346e9 Move connected notification setting to its own category 2025-09-29 14:00:12 +02:00
darken f2e6d8978c Fix false unsaved changes detection in profile editor
- Replace multiple StateFlows with single ProfileEditorState data class
- Store ByteArray keys as hex strings to enable proper equality comparison
- Fix race conditions in state initialization by setting initial and current state atomically
- Simplify change detection logic to simple data class comparison

The issue was caused by:
1. ByteArray types not implementing proper equals() (compared by reference)
2. Race conditions between multiple StateFlow updates
3. Complex change detection logic with timing issues

Now uses atomic state management with proper value-based equality.
2025-09-29 13:37:01 +02:00
darken fbbbf751c5 Add priority hint to device profiles list
- Shows hint when 2+ profiles exist explaining order determines priority
- Appears as flat list item at bottom, less intrusive than card
- Prevents dragging hint item, keeps it at bottom
- Uses book icon for informational guidance
2025-09-29 13:37:01 +02:00
darken 132b3996fc Update signal quality slider range
The signal quality slider in `device_profile_creation_fragment.xml` now ranges from 0 to 100 with a step size of 1, previously 10 to 100 with a step size of 5.
2025-09-29 13:37:01 +02:00
darken 7b41f83a53 Refactor device profiles and settings migration
This commit introduces a new device profile system, moving away from a single "main device" configuration in `GeneralSettings`.

Key changes:
- `DeviceProfilesRepo` now manages a list of `DeviceProfile` objects, allowing for multiple device configurations.
- On first launch with this update, existing "main device" settings from `GeneralSettings` (address, model, keys, signal quality) are migrated into a new default `AppleDeviceProfile`.
- The `MonitorWorker`, `PopUpReaction`, `AutoConnect`, and `PodHistoryRepo` have been updated to use the new `DeviceProfilesRepo` instead of the old `GeneralSettings` for device-specific information.
- `AppleFactory` now uses `DeviceProfilesRepo` to find matching profiles based on IRK.
- `TroubleShooterFragmentVM` now interacts with `DeviceProfilesRepo` for profile management during troubleshooting.
- The `create` method in `ApplePodsFactory` and its implementations are now `suspend` functions to allow for asynchronous operations like fetching profiles from the repository.
- Old "main device" preference keys in `GeneralSettings` have been renamed with an "old" prefix and will be removed in a future update.
- A new `currentProfiles()` extension function provides a convenient way to get the current list of profiles.
2025-09-29 13:37:01 +02:00
darken b6027186c0 Distinguish between UNKNOWN and missing profile
This commit modifies the `AppleFactory` to correctly handle `PodDevice.Model.UNKNOWN`.

Previously, if a device's model was `UNKNOWN`, it wouldn't match any profile and `isIRKMatch` would be false. This change ensures that `isIRKMatch` reflects the actual IRK match status, regardless of whether a specific profile is found.

Devices with an `UNKNOWN` model can now match profiles that are also marked as `UNKNOWN`, allowing for generic profile matching when the specific model isn't identified.
2025-09-29 13:37:01 +02:00
darken d2d126e9d9 Enable per-app language preferences
This commit enables the automatic generation of per-app language preferences by configuring the Android Gradle Plugin.

A `resources.properties` file is added to specify `en` as the `unqualifiedResLocale`, which serves as the default/fallback locale.

Additionally, `.kotlin` is added to `.gitignore` to exclude Kotlin build-related files.
2025-09-29 13:37:01 +02:00
darken 89632d864a Remove deviceModel property from ApplePodsFactory interface
Removed the deviceModel property from ApplePodsFactory interface and all 25 implementing classes across AirPods, Beats, and misc device factories. This simplifies the factory pattern as the model information can be derived from the concrete implementation type itself.
2025-09-29 13:37:01 +02:00
darken 2cd209ee03 Add TODO for profile migration
This commit adds a TODO comment to `GeneralSettings.kt` to migrate settings related to signal quality and main device details to the new device profiles system.
2025-09-29 13:37:01 +02:00
darken b96d6ef4db Improve device profile creation UI and fix navigation bar overlaps
- Fix navigation bar overlap issues in profile creation and list screens
- Add proper edge-to-edge handling with dynamic padding for navigation bars
- Improve paired device selection with "None" option and clear functionality
- Add localized error messages for key validation with proper placeholders
- Centralize default signal quality value in DeviceProfile companion object
- Add unsaved changes detection and confirmation dialogs
- Enhance drag handle touch target size with FrameLayout wrapper
- Restore device address in profile editing and improve change tracking
- Localize all user-facing strings following project guidelines
2025-09-29 13:37:01 +02:00
darken d6af8e21cc Remove TitleLarge from device profile creation toolbar
The `titleTextAppearance` was removed from the toolbar in `device_profile_creation_fragment.xml` to use the default appearance.
2025-09-29 13:37:01 +02:00
darken ae2d1c6868 Refactor: Rename devices package to profiles
This commit renames the `devices` package and its contents to `profiles` for better clarity and consistency. This includes renaming classes, files, and updating import statements and navigation graph references.
2025-09-29 13:37:00 +02:00
darken 98cf4ff3e7 Refactor device profiles: Add creation UI and drag-to-reorder
This commit introduces a new UI for creating and editing device profiles. Users can now define profile names, select device models, set minimum signal quality, and optionally add identity and encryption keys.

The device manager screen now supports drag-and-drop reordering of profiles. The order of profiles in the list now determines their priority, with items at the top having higher priority. The internal data structure for device profiles has been updated, and the `PodMonitor` now sorts devices based on this new profile priority.
2025-09-29 13:37:00 +02:00
darken 679c9cd070 wip 2025-09-29 13:36:59 +02:00
darken 5b281303d9 Use Uri.toUri() extension
This commit updates the `OverviewFragment` to use the `androidx.core.net.toUri()` extension function when creating Uris for system settings intents. This replaces the direct usage of `Uri.parse()`.
2025-09-29 13:36:46 +02:00
darken cb28bbfaf3 Remove item count debug subtitle in OverviewFragment
The subtitle showing the number of items in the list was only visible in debug builds and has been removed.
2025-09-29 13:36:46 +02:00
darken cd190389cd Update all device implementations to support profiles
- Update all Apple device classes to use AppleMeta with profile support
- Modify device factories to inject profile information during creation
- Update UI components to handle profile-aware device metadata
- Refactor reaction systems to work with profile-based devices
- Update monitor worker and cache to support profile relationships
- Ensure consistent meta structure across all device types

All devices now properly reference their associated profiles through
the meta.profile field, enabling profile-based functionality.
2025-09-29 13:36:46 +02:00
darken 0af469af07 Add core device profile infrastructure
- Implement DeviceProfile interface with sealed interface pattern
- Add AppleDeviceProfile for Apple devices with IRK support
- Create DeviceProfilesRepo for profile persistence with Moshi
- Add NameBasedPolyJsonAdapterFactory for polymorphic JSON serialization
- Update PodDevice.Meta to include profile reference
- Extend ApplePods with AppleMeta containing profile information
- Add utility extensions for profile-based device filtering

This establishes the foundation for device profile management system.
2025-09-29 13:36:46 +02:00
darken 1689153070 Implement profile-based device separation in overview UI
- Add UnmatchedDevicesCard to show/hide devices without profiles
- Separate devices with profiles from unmatched devices in overview
- Add priority-based sorting (profile.priority with 0 = highest)
- Add compiler args for experimental unsigned types and annotation targets
- Add string resources for unmatched devices UI
- Refactor overview to handle both profiled and non-profiled devices

The UI now shows:
1. Devices with configured profiles first (sorted by priority)
2. Collapsible section for unmatched devices with toggle button
3. Session-persistent show/hide state for unmatched devices
2025-09-29 13:36:46 +02:00
darken 520458b107 Consolidate strings.xml files from app-common to app module
- Merged all 76 locale strings.xml files from app-common to app module
- Removed app-common module dependency from app/build.gradle.kts
- Removed app-common from settings.gradle
- Fixed all BuildConfig and R class import statements
- Updated fully qualified R references from eu.darken.capod.common.R to R
- Deleted entire app-common directory and all associated files
- Added temporary fallback for missing GITSHA BuildConfig field

The project now has a simplified structure with all string resources
consolidated in the app module, eliminating the unnecessary app-common
module while maintaining full functionality.

Builds successfully for both FOSS and Google Play variants.
2025-09-29 13:36:46 +02:00
darken be8f4919c8 Merge main and common module to simplify. 2025-09-29 13:36:43 +02:00
darken deb012600d Remove "Show all devices" setting and always show all devices.
This commit removes the "Show all devices" toggle from General Settings. The app will now always display all nearby Bluetooth devices in the overview, simplifying the UI and device discovery.

The corresponding preference `core.showall.enabled` and its usage have been removed. The debug setting `showUnfiltered` no longer affects this behavior.
2025-09-29 13:36:21 +02:00
darken 15371e01ef Refactor device configuration UI and move troubleshooter
- Change "No primary device" card to "No device configured" with clearer messaging
- Replace troubleshoot action with "Manage devices" button that navigates to device manager
- Move troubleshooter from overview to Settings → Support section
- Add concise troubleshooter description for settings preference
- Update navigation to support troubleshooter access from settings
- Improve user experience by providing more intuitive device management flow
2025-09-29 13:36:21 +02:00
darken 0e3ee655ef Remove WearOS variant (hasn't been supported for quite some time). 2025-09-29 13:36:21 +02:00
darken da5af8e4b1 Release: 2.19.0-rc1 2025-09-21 19:22:38 +02:00
Matthias Urhahn 6c848eaa55 Merge pull request #328 from d4rken-org/airpods-pro-3-merged
AirPods Pro 3 support
2025-09-21 19:13:57 +02:00
darken 15054d836d Merge branch 'airpods-pro-3' into airpods-pro-3-merged
# Conflicts:
#	app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
#	app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro3.kt
2025-09-21 18:38:13 +02:00
darken 0ac9bba482 Merge branch 'airpods-pro-3' into airpods-pro-3-merged
# Conflicts:
#	app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
#	app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro3.kt
2025-09-21 18:33:10 +02:00
Tharen Abela 247bc13e0a Initial support for AirPods Pro 3
This commit introduces the AirPods Pro 3 model to the application, including its representation in the README, the PodDevice interface, and the AppleFactoryModule.

A new class for AirPods Pro 3 is created, along with corresponding unit tests to validate its functionality.

Based on the AirPods Pro 2 USBC.
2025-09-21 17:56:03 +02:00
li-fstz 33126be817 Add support for AirPods Pro 3 2025-09-21 17:55:56 +02:00
darken b02b31043b Release: 2.18.0-rc1 2025-09-03 19:41:29 +02:00
darken 6786b5aa8a Remove Norwegian (Bokmål) from supported languages. 2025-09-03 19:41:12 +02:00
darken 8577e7bae8 Release: 2.18.0-rc0 2025-09-03 19:08:48 +02:00
darken df88399e7a Remove BUILDTIME BuildConfig field
This commit removes the `BUILDTIME` field from `BuildConfig` across all modules. This change improves build caching and reproducibility by eliminating a value that changed with every build.

The `BUILDTIME` was previously used in `VERSION_DESCRIPTION_LONG` but is no longer included.
2025-08-31 15:30:12 +02:00
darken 16392d5721 Refactor buildSrc and update dependencies
This commit refactors the `buildSrc` module by:
- Removing the `setupLibraryDefaults()` extension function from `ProjectConfig.kt` as its functionality is largely covered by standard Gradle plugin configurations.
- Inlining version numbers for Moshi and various testing libraries directly into `Dependencies.kt`, removing them from `Versions.kt`. This simplifies version management for these specific libraries.
- Removing unused version declarations from `Versions.kt`.

Additionally, this commit updates the following dependencies:
- Kotlin from 2.1.21 to 2.2.10
- Android Gradle Plugin from 8.11.0 to 8.12.2
- KSP from 2.1.21-2.0.1 to 2.2.10-2.0.2
- Dagger from 2.56.2 to 2.57.1
- AndroidX Navigation from 2.8.9 to 2.9.3

The `targetSdk` was also removed from the `app-common` module's `defaultConfig` as it's typically inherited or set at the app level.
2025-08-31 15:30:12 +02:00
darken 2edf5411ce Allow keeping "connected" notification after disconnect
This commit introduces a new setting that allows users to keep the "connected" notification visible even after the monitored device disconnects.

When this setting is enabled and the "Extra notification" is also active, the `MonitorWorker` will no longer cancel the connected notification upon finishing. This provides users with the option to see the last known battery levels after disconnection.

A new preference `keepConnectedNotificationAfterDisconnect` is added to `GeneralSettings` and the UI.
2025-08-31 10:11:45 +02:00
darken 89e46d3858 Update translations 2025-08-30 17:12:36 +02:00
darken da45eb5dda Update translations 2025-08-30 17:12:36 +02:00
darken a1868f495d Update translations 2025-08-30 17:12:36 +02:00
darken ceb5034c3b Update translations 2025-08-30 17:12:36 +02:00
darken 02ff7089c0 Refactor GPlay billing error handling and translations
This commit introduces localized error messages for Google Play billing issues and refactors the error handling in `UpgradeRepoGplay`.

- `BillingException` and `BillingResultException` now implement `HasLocalizedError` to provide user-friendly error messages.
- New string translations for billing error labels and descriptions have been added for various languages.
- `UpgradeRepoGplay` now includes an `error` field in its `Info` state to propagate billing errors.
- The `upgradeInfo` flow in `UpgradeRepoGplay` has been updated with a `retryWhen` operator to handle errors more gracefully. If an error occurs but the pro state was recent, it emits a grace period state. Otherwise, it emits the error.
- `OverviewFragmentVM` now observes `upgradeState` and posts any non-pro errors to `errorEvents` for display.
2025-08-30 17:12:36 +02:00
darken 2fa33b13a3 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.claude/settings.json
2025-08-30 16:48:41 +02:00
darken 96a654638a Claude settings 2025-08-30 16:48:22 +02:00
darken b4aa7b0815 Claude settings 2025-08-30 16:48:04 +02:00
darken df5389c27e Configure Jekyll include and exclude settings
This commit updates the Jekyll configuration file (`_config.yml`) to:
- Include `README.md` and `CHANGELOG.md` in the site build.
- Exclude `CONTRIBUTING.md` and `CLAUDE.md` from the site build.
2025-08-30 16:44:26 +02:00
darken 0611ec0e08 Add CLAUDE.md to guide AI code generation
This commit introduces a `CLAUDE.md` file.
This file contains instructions and context for the Claude AI to assist with development in this repository. It includes:
- Common build, test, and code quality commands.
- An overview of the project's multi-module architecture, core patterns (MVVM, DI with Hilt, Coroutines, Repository), and key components like `PodMonitor` and the reaction system.
- Details on build flavors (FOSS, Google Play) and build types (debug, beta, release).
- A description of the data flow architecture.
- Information on the testing strategy and key dependencies.
- Development notes regarding Bluetooth LE implementation and multi-platform considerations for phone and Wear OS.
2025-08-30 16:42:28 +02:00
darken c325112b44 Apply translation updates and add Norwegian Bokmål
This commit incorporates various translation improvements across multiple languages (Ukrainian, Greek, French, Turkish, Spanish (Mexico), Arabic, Korean, Urdu (India), Indonesian, Romanian, Zulu) for UI strings and Play Store metadata.

Additionally, this commit introduces new translations for Norwegian Bokmål (nb) for the Play Store listing (title, short description, full description) and adds initial string resources for Romansh (rm).
2025-08-30 16:42:28 +02:00
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
darken 513bd891af Release: 2.13.0-rc0 2024-02-25 13:30:10 +01:00
darken 37cd753ab8 Update README 2024-02-25 13:29:13 +01:00
darken 34bf2311b4 Add support for PowerBeats4
Thanks to @docgalaxyblock
2024-02-25 13:29:13 +01:00
darken a86093f617 Update translations 2024-02-25 13:16:19 +01:00
Matthias Urhahn 71f66e6004 Merge pull request #182 from JerretQueen/main
Onboarding typo fixed, dual pods layout realigned.
2024-02-25 13:12:31 +01:00
Matthias Urhahn 2d1ab85e01 Update README.md 2024-02-25 13:11:46 +01:00
Matthias Urhahn f029eb9102 Merge pull request #179 from hwlnx/patch-1
Update strings.xml
2024-02-25 13:06:15 +01:00
Matthias Urhahn 471837ab88 Update gradle-wrapper.properties 2024-02-25 13:05:05 +01:00
Matthias Urhahn 1e80d742cc Update build.gradle.kts 2024-02-25 13:04:38 +01:00
Matthias Urhahn 16b6273938 Update build.gradle.kts 2024-02-25 13:04:08 +01:00
JerretQueen 3407370563 1. Fixed a typo in @string/onboarding_body2. The message now correctly says "implementation" instead of "implemtation".
2. Changed layout of overview_pods_dual_item.xml so that the left and right pod status are properly lined up with each other.
2023-12-30 00:14:24 -06:00
hwlnx 3cda73a36c Update strings.xml 2023-11-19 20:05:42 +01:00
darken b604c423ad Release: 2.12.1-rc0 2023-10-28 19:51:26 +02:00
darken 0c8b121c2c Request FOREGROUND_SERVICE_SPECIAL_USE for debug log service 2023-10-28 19:50:56 +02:00
darken da11961611 Release: 2.12.0-rc0 2023-10-28 18:36:11 +02:00
darken 48b9952fc0 Update translations 2023-10-28 18:21:50 +02:00
darken 99fa99eba5 Update billing client 2023-10-28 18:03:28 +02:00
darken 06e67e04ac Move permission 2023-10-28 17:58:09 +02:00
darken 4a2f7c20a2 Update unit test 2023-10-28 17:58:09 +02:00
darken 82e8769407 Bump dependencies and API targets. 2023-10-28 17:58:09 +02:00
darken ad38702df1 Widget: Center device label 2023-10-28 16:59:52 +02:00
darken 3f98c62a8f Extend test case for AirPods Pro 2 (USB-C)
Closes #164
2023-09-24 12:01:15 +02:00
darken ec7447992f Release: 2.11.1-rc0 2023-09-23 21:44:04 +02:00
Matthias Urhahn 99958e13eb Update translations (#166) 2023-09-23 21:43:24 +02:00
Matthias Urhahn b9fe5d4a14 Update README.md 2023-09-23 21:38:44 +02:00
darken 1443fd5714 Support for Apple AirPods Pro2 with USB-C
See #164
2023-09-23 21:35:30 +02:00
darken 798083b235 Remove link from changelog notes (eggshells) 2023-09-11 23:35:20 +02:00
darken 18a7311089 Release: 2.11.0-rc0 2023-09-11 22:45:47 +02:00
darken 9f49d62f12 Clean up store description and remove WearOS sentences. 2023-09-11 21:09:02 +02:00
darken 0c3c4e7ea1 Update translations 2023-09-11 21:09:02 +02:00
darken 7cdf653842 Remove WearOS part from app description, it's no longer on Google Play. 2023-09-11 21:09:02 +02:00
darken 32d99eada8 Prevent app from crashing if no app to view urls is installed 2023-09-11 20:57:15 +02:00
darken 9e4adb16c6 Hide install, not useful without 3rd party bug tracker 2023-09-11 20:33:06 +02:00
darken 4b5638e4f0 Hide bug reporting setting (but lets keep the setting ID because if we re-add we might use the setting state again). 2023-09-11 20:32:53 +02:00
darken d8fb9cdd30 Remove bugsnag 2023-09-11 20:32:53 +02:00
Matthias Urhahn 3920489dcd Extra onboarding step (#158)
* wip

* Add extra onboarding step
2023-09-11 20:32:41 +02:00
Matthias Urhahn 09d013588d Update PRIVACY_POLICY.md 2023-09-11 01:33:38 +02:00
darken d8bd03c65d Remove WearOS build from Google Play pipeline. 2023-07-16 01:05:04 +02:00
darken 8b2c6b9498 Release: 2.10.0-rc2 2023-07-07 10:13:48 +02:00
darken 0f63d3a0d4 Revert "Don't bump WearOS, not needed due to new track system?"
This reverts commit a321bb733c.
2023-07-07 10:12:56 +02:00
darken 672eb333b0 Update fastlane 2023-07-07 10:12:24 +02:00
darken de609b4d05 Release: 2.10.0-rc1 2023-07-07 09:05:38 +02:00
darken 7e6e9eee36 Try WearOS first as it is more likely to fail. 2023-07-07 09:04:34 +02:00
darken a321bb733c Don't bump WearOS, not needed due to new track system? 2023-07-07 09:04:34 +02:00
darken 490369a3e1 Adjust CI to build and upload to WearOS beta/production tracks 2023-07-07 09:04:34 +02:00
darken a2cce044ef Release: 2.10.0-rc0 2023-07-05 09:16:53 +02:00
darken 8884bb88c7 Update translations 2023-07-05 09:15:44 +02:00
darken 313b0e9131 Include changelog in changelog. 2023-07-05 09:15:44 +02:00
darken 47048cf7e7 Update translations 2023-07-05 09:15:44 +02:00
darken ef1c4615aa Add reaction option: Popup on new connection 2023-07-05 08:21:37 +02:00
Matthias Urhahn c9587b2bf0 Update translations (#147)
* Update translations

* Update translations
2023-07-04 12:41:14 +02:00
Sean Singh 669fa2fd22 Support edge-to-edge rendering on Android Q+ (#146)
* Set nav bars transparent in themes.xml files

* App renders edge-to-edge; need to handle scroll overlaps

* Add fake padding

* Begin version separating

* Edge-to-edge working everywhere except main page

* Finalized

* Cleanup Finalized

* Cleanup Finalized

* Removed redundant values-v29 & optimize imports

* Consolidated insets to Fragment2

* Consolidated insets to Fragment2 (file wasn't staged)
2023-07-04 12:17:44 +02:00
darken fb8ec72825 Release: 2.9.3-rc0 2023-06-27 22:07:22 +02:00
Matthias Urhahn d1687f574f Merge pull request #145 from d4rken-org/bump_workmanager
Fix background execution errors.
2023-06-27 22:04:28 +02:00
darken 72f47bec1f Fix background execution errors.
Bump workmanager `2.7.1` -> `2.8.1` to fix

```java
android.app.ForegroundServiceStartNotAllowedException: startForegroundService() not allowed due to mAllowStartForeground false: service eu.darken.capod/androidx.work.impl.foreground.SystemForegroundService
```
2023-06-27 21:55:58 +02:00
Matthias Urhahn 20e8aecf83 Merge pull request #144 from d4rken-org/gplay_hates_splash
Fix "Missing app icon in splash screen"
2023-06-27 21:49:50 +02:00
Matthias Urhahn ee41474670 Merge pull request #143 from d4rken-org/gplay_wear_policies_fml
Fix "Missing show time"
2023-06-27 21:49:37 +02:00
darken ac29e28eda Fix "Missing app icon in splash screen"
> Your app does not show a 48x48dp app icon on a black background during app startup. For more information, see Branded launch.
2023-06-27 21:49:05 +02:00
darken 58b96cbdee Fix "Missing show time"
> Your app does not display the time of day clearly at the top of the app home screen and any ongoing activity screens. We recommend that you display the time of day at the top of all activities except dialog and confirmation screens. For more information, see Show the time.
2023-06-27 21:12:16 +02:00
darken 0c1fc15251 Update translations 2023-06-27 20:28:31 +02:00
darken 7b8e0cc952 Update translations 2023-06-27 20:28:31 +02:00
darken c8aa591feb Update AGP 2023-06-27 20:28:21 +02:00
darken afa4d6e807 Release: 2.9.2-rc0 2023-05-16 09:23:59 +02:00
darken e6b7eda9e7 Update store translations 2023-05-16 09:23:09 +02:00
darken bbc4e05c8f Update translations 2023-05-16 09:23:09 +02:00
darken 50bccbaa2e Update Crowdin tooling 2023-05-16 09:23:09 +02:00
Matthias Urhahn d71e83a2af Merge pull request #132 from d4rken-org/tweak_widget_layout
Tweak widget layout
2023-05-16 09:14:30 +02:00
Matthias Urhahn 92a67702b7 Merge pull request #131 from d4rken-org/gradle_bump
Gradle bump
2023-05-16 09:04:46 +02:00
darken 2f9185efa7 Tweak dual pod widget.
Less padding to reduce cutoff chances and put the label at the type bottom as it's the least important info.
2023-05-16 09:00:58 +02:00
darken a10216520b Fix resource reference 2023-05-16 08:59:54 +02:00
darken d5354e5156 Fix resource reference 2023-05-16 08:55:35 +02:00
darken a8f9b1f732 Use non final resource ids 2023-05-16 08:07:48 +02:00
darken 08a796e621 Fix wear namespace 2023-05-16 08:05:59 +02:00
darken 15f3a68f8e Use non transitive R classes to improve performance 2023-05-16 07:58:28 +02:00
darken e0bc6dafa0 Bump gradle 2023-05-16 07:46:13 +02:00
darken 430c1236d7 Fix typo 2023-05-07 20:15:41 +02:00
darken 3ac357161a Improve strings based on translator feedback 2023-05-07 20:15:41 +02:00
darken 3c5f1bf21f CI: Remove unused base flavor, all our modules are flavored :) 2023-04-07 11:00:44 +02:00
darken 5544cd3770 CI: Update action dependencies, improve naming and use more specific build commands 2023-04-07 11:00:44 +02:00
darken 71a060feb9 Release: 2.9.1-rc0 2023-04-05 16:19:46 +02:00
darken 25d643257c Update translations 2023-04-05 13:13:42 +02:00
darken 85900e5bff Improve final headphones checks and fix IllegalStateException if data changes between the final two checks. 2023-04-05 13:13:42 +02:00
darken d3257d6c5e Improve troubleshooting texts 2023-04-05 13:13:42 +02:00
darken afe1399f72 Release: 2.9.0-rc0 2023-04-02 22:25:17 +02:00
Matthias Urhahn 20ce2d3e22 A troubleshooter that automatically goes through different compatibility settings. (#125) 2023-04-02 22:22:15 +02:00
Matthias Urhahn cea8f2290d More IAP improvements (#121)
* More IAP improvements

* More refactoring

* More tolerance towards GPlay API issues.

* Bump billing dependency

* Migrate away from deprecated methods.
2023-03-20 08:21:54 +01:00
darken 1b5b59c41b Release: 2.8.4-rc0 2023-03-13 17:28:28 +01:00
Matthias Urhahn 8df7c19e84 Fix billing client disconnection handling (#117) 2023-03-13 17:28:01 +01:00
darken 88b0069b94 Release: 2.8.3-rc0 2023-03-13 13:48:11 +01:00
Matthias Urhahn 3b8ddc9f06 Fix reversed AirPods Max wear-detection (#116)
* Fix reversed AirPods Max wear-detection
Fixes #115

* Remove unnecessary test
2023-03-13 13:47:32 +01:00
darken 708c7f38ec Merge remote-tracking branch 'origin/main' into main 2023-03-12 14:05:25 +01:00
darken 139daa1cc2 Update full gplay text 2023-03-12 14:05:20 +01:00
Matthias Urhahn b85ac778c0 Address that gplay api can emit multiple times (#114) 2023-03-12 02:58:37 +01:00
Matthias Urhahn a3e2c9e61d Bump dependencies and gradle (#113) 2023-03-11 18:16:24 +01:00
Matthias Urhahn ad78a0570c For some reason we can finish the widget update broadcast twice. (#112)
I can't yet determine why this race-condition occurs, due to the forced timeout, it shouldn't be due to exceeding the receiver ANR window...
2023-03-11 13:01:15 +01:00
Matthias Urhahn 7e6ee274d8 Quick data updates can cause concurrent access and modifying of the notification builder. (#111)
Internally building the notification can then throw a ConcurrentModificationException.
2023-03-11 13:01:05 +01:00
Matthias Urhahn daf7411c36 Show worker foreground notification early. (#110) 2023-03-11 13:00:55 +01:00
darken d3dedbe6cd Release: 2.8.2-rc0 2023-03-10 12:13:30 +01:00
Matthias Urhahn d5411586d5 Update translations (#109)
* Update translations, add Portuguese

* Update translations
2023-03-10 12:12:31 +01:00
Matthias Urhahn 94e2d40554 Fix AirPods Max wear detection. (#108)
Closes #106
2023-03-10 11:30:40 +01:00
Matthias Urhahn db80fa1b46 Wear: Remove unused code (#104)
* Wear: Remove unused code

* Remove more code
2023-03-02 16:39:11 +01:00
Matthias Urhahn 9b569b8d07 Update Turkish translation (#100) 2023-02-20 09:39:27 +01:00
darken 90288880fb Add f-droid as source 2023-02-20 09:14:20 +01:00
darken 870e518ff6 Release: 2.8.1-rc0 2023-02-09 22:01:02 +01:00
darken 6d89169bae Update release.sh script to create default changelogs for f-droid 2023-02-09 21:59:44 +01:00
darken ac77ee2703 Update default changelog for GPlay 2023-02-09 21:53:33 +01:00
darken 18dcf603e9 Release: 2.8.0-rc0 2023-02-03 22:32:01 +01:00
Matthias Urhahn 71aaba1b86 Fix data display for non dual apple pods (#98)
* Fix data display for fake pods and beats devices (with two pods)

* Rename methods
2023-02-03 22:31:23 +01:00
Matthias Urhahn 64b9d5f17a Remove ~ and % from signal quality value, it's not necessarily a 0 to 100 percent indicator. (#96) 2023-02-02 16:47:06 +01:00
Matthias Urhahn a9f23c4bfe Change defaults setting for "Show all devices" (#95)
* Fix potential widget issue where a broadcast race condition causes the goAsync() to fail.

* Enable "show all devices" by default to help new users.
2023-02-02 16:46:52 +01:00
Matthias Urhahn f454f46778 Fix potential widget issue where a broadcast race condition causes the goAsync() to fail. (#94) 2023-02-02 16:46:36 +01:00
Matthias Urhahn 432936ea2c Merge pull request #93 from d4rken-org/crowd_reliability
Improve tracking in crowds
2023-02-02 16:46:08 +01:00
Matthias Urhahn 8211b0ac88 Track case battery state outside of history list and simplify history clean up. (#92) 2023-02-02 16:45:39 +01:00
Matthias Urhahn c314987e13 Fix popup icon sizes (#91) 2023-02-02 16:45:22 +01:00
Matthias Urhahn fc6cd74752 If the current pods don't have a lid state value, and there is none in recent history, we can assume NOT_IN_CASE. (#90) 2023-02-02 16:45:05 +01:00
Matthias Urhahn a47011e579 Merge branch 'main' into crowd_reliability 2023-02-02 14:57:04 +01:00
darken fbeeec6539 Boost signal quality rating by up to 25% depending on how long they have been tracked. 2023-02-02 14:46:29 +01:00
darken e1cd79cd9d Cap reliability at 100% 2023-02-02 14:45:44 +01:00
darken 692ced1ee7 Timeout after 30 not 20 seconds. 2023-02-02 14:45:22 +01:00
darken c5cc127df4 Smooth rssi values and use reliability (missed signals) as boost, not as factor. 2023-02-02 11:50:33 +01:00
Matthias Urhahn 5f00f10ae8 Remove short hash from generated apk file names (#89) 2023-02-02 08:38:22 +01:00
Matthias Urhahn 0fc9716f06 Only apply Bugsnag to gplay build flavor (#88)
* wip

* Setup bugsnag only in gplay builds.

* Bugsnag does not support uploading mapping files or build information from library modules. This should be done from the application module which produces your APK instead.

* Only apply Bugsnag plugin for the right flavors.

* Run unit tests in debug, otherwise we run issues requiring api keys in the env
2023-02-01 11:17:50 +01:00
darken 48fd3543e3 Release: 2.7.2-rc0 2023-01-30 13:38:53 +01:00
Matthias Urhahn 41985794c2 Adjust supported AirPods Max features. (#86)
Remove state detection (was UNKNOWN when it should have been MUSIC).
Add PodStyle detection.
2023-01-30 13:37:40 +01:00
Matthias Urhahn 87a92fd001 Fix feature display for single pods inside the app. (#87) 2023-01-30 13:37:31 +01:00
darken f6f53f2a15 Release: 2.7.1-rc1 2023-01-28 22:14:47 +01:00
Matthias Urhahn 2531769b79 Update translations (#85)
Add arabic and update chinese (TW)
2023-01-28 22:12:25 +01:00
darken e9fa9f6cb8 Release: 2.7.0-rc0 2023-01-25 16:54:32 +01:00
Matthias Urhahn b98ca86886 Faster CI checks (#83)
* Parallelize CI code checks

* Only assemble debug variant, otherwise we need to setup signing.
2023-01-25 16:47:46 +01:00
Matthias Urhahn d917c8ffe6 New compatibility option for receiving BLE data (#82)
* Improve compat options: Add alternative method for receiving BLE scan results via PendingIntents

* Add missing logtags

* Reduce log spam

* Make the linter happy
2023-01-25 16:27:29 +01:00
Matthias Urhahn 5d44737199 Merge pull request #80 from d4rken-org/compatibility_options
Improve compatibility options
2023-01-25 14:56:10 +01:00
Matthias Urhahn 6e8693a6b0 Merge pull request #81 from d4rken-org/modularize_permissions
Move shared permissions into the common module
2023-01-25 14:55:54 +01:00
darken 6d9e7404a1 Improve compatibility options 2023-01-25 14:46:20 +01:00
darken 70ceb389b5 Move shared permissions into the common module. 2023-01-25 14:44:38 +01:00
darken 832e364a2f Update translations 2023-01-25 09:26:50 +01:00
darken 51f9c5c7d8 Release: 2.6.1-rc0 2023-01-20 12:55:28 +01:00
Matthias Urhahn 15141c8528 More granular pod features (#76)
* Make pod features more granular
Don't assume all AirPods/Beats return a correct connection state.

* Refactoring

* Fix refactoring regression

* AirPods Gen1 don't support state detection
2023-01-18 22:15:38 +01:00
Matthias Urhahn 0cdac10fd4 Update README.md 2023-01-14 16:45:11 +01:00
darken aaab548bfd Release: 2.6.0-rc0 2023-01-14 16:44:16 +01:00
Matthias Urhahn 46611b5606 Add preliminary support for Beats Fit Pro (#75) 2023-01-14 16:35:04 +01:00
Matthias Urhahn fdbdb46577 Overhaul play/pause reaction code (#74) 2023-01-14 16:21:57 +01:00
darken e7fd81d0eb Release: 2.5.3-rc1 2023-01-12 12:51:46 +01:00
darken ce50bae6a0 Merge branch 'main' of github.com:d4rken-org/capod into main 2023-01-12 12:51:03 +01:00
darken 8331cf3866 Release: 2.5.3-rc0 2023-01-12 12:50:53 +01:00
Matthias Urhahn 670262e53b Update README.md 2023-01-12 12:50:44 +01:00
Matthias Urhahn c584f942cf Support for fake AirPods 2. Generation (#73) 2023-01-12 12:32:44 +01:00
darken e83b6bb583 Update readme 2023-01-12 12:11:46 +01:00
darken cc5de668f0 Add additional test data for fake AirPods Pro2 2023-01-12 12:07:24 +01:00
darken 653c7b8c4b Release: 2.5.2-rc0 2023-01-06 16:45:28 +01:00
darken 4e38bcd4cc Fix left pod icon tint. 2023-01-06 16:44:30 +01:00
Matthias Urhahn fcbeda4888 Improve feature support for fake AirPods Gen3 (#72)
Remove microphone detection and add ear detection.
2023-01-06 16:40:03 +01:00
Matthias Urhahn 4101fddc5b Improve feature support for fake AirPods Gen1 (#71)
Remove microphone detection and add ear detection and pod charge detection.
2023-01-06 16:39:46 +01:00
darken 86df7bea22 Improve feature support for fake AirPods Pro
Remove microphone detection and add pod charging and ear detection.
2023-01-06 16:02:38 +01:00
darken 4db8c0c286 Add support for fake AirPods Pro 2 2023-01-06 16:02:25 +01:00
darken 1d03335903 Improve dual pod widget layout, use same style as notification. 2023-01-02 18:26:33 +01:00
darken b1b541014f Release: 2.5.1-rc0 2022-12-24 11:57:34 +01:00
darken b40a7e435f Update translations 2022-12-24 11:56:46 +01:00
Matthias Urhahn 8305fb4c5f Airpod pro2 icon iteration (#64)
* Update airpods pro icons to SVG variant

* Fix AirPodsPro2 not using the new icons

* Revert "Update airpods pro icons to SVG variant"

This reverts commit d7ca13d439.

* Add padding to icons
2022-12-24 11:48:32 +01:00
darken 6c678be511 Release: 2.5.0-rc1 2022-12-23 09:54:19 +01:00
darken 2823663f09 Merge branch 'main' of github.com:d4rken-org/capod into main 2022-12-23 09:53:08 +01:00
darken 033c2264f8 Release: 2.5.0-rc0 2022-12-23 09:52:59 +01:00
darken 8eb6ce19f7 Add support for specific left/right/case icons.
Add AirPods Pro 2 Icons by @christoschronopoulos
2022-12-23 09:52:17 +01:00
darken 1cfc4e6f65 Merge branch 'improve_fake_airpod_support' into main 2022-12-23 08:27:00 +01:00
darken 454d76e305 Release: 2.4.0-rc0 2022-12-19 17:10:30 +01:00
darken f3609fc3a5 Update translations 2022-12-19 17:09:55 +01:00
darken 16da51d095 Refactor fake airpods code and improve tests 2022-12-19 17:09:55 +01:00
darken 51fb4c7a29 Update translations 2022-12-19 17:03:17 +01:00
darken 0b27cb55dc Refactor fake airpods code and improve tests 2022-12-19 17:03:17 +01:00
darken 9e836eef69 Add support for fake AirPods Gen3 2022-12-19 16:32:09 +01:00
1118 changed files with 22819 additions and 10827 deletions
+45
View File
@@ -0,0 +1,45 @@
# CAPod - Companion for AirPods
Android app that detects and monitors AirPods via Bluetooth LE. Displays battery levels, triggers popup notifications on case open, and provides home screen widgets.
## Project Structure
| Module | Description |
|--------|-------------|
| `app/` | Main Android app (FOSS and Google Play flavors) |
| `app-common/` | Shared code between phone and Wear OS apps |
## Build Flavors
- **FOSS** (`foss`): Open-source, no Google Play dependencies
- **Google Play** (`gplay`): Includes billing client for IAP
Quick build check: `./gradlew assembleFossDebug`
## Key Locations
| Path | Contains |
|------|----------|
| `app/src/main/java/` | Main app source (activities, fragments, services) |
| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) |
| `app/src/main/res/` | Layouts, drawables, strings |
| `app-common/src/test/` | Unit tests |
| `app/build.gradle.kts` | App build config, dependencies, flavors |
## Development Tips
- Use `assembleFossDebug` as the fastest build variant for iteration
- Shared code goes in `app-common/`, app-specific code in `app/`
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
- Always use string resources for user-facing text (see localization rules)
- Check `git log --oneline -20` for commit message style before committing
## Rules Reference
Detailed guidelines are in `.claude/rules/`:
- `architecture.md` — Module structure, key components, data flow, dependencies
- `build-commands.md` — Build, test, lint, and release commands
- `localization.md` — String resource naming conventions
- `commit-guidelines.md` — Commit message format and prefixes
- `agent-instructions.md` — Sub-agent delegation and critical thinking
+39
View File
@@ -0,0 +1,39 @@
---
description: Instructions for Claude Code sub-agents and task delegation
globs:
- "**"
---
# Agent Instructions
## Critical Thinking
- Do not blindly accept information at face value
- Verify assumptions against actual code before proceeding
- When encountering unexpected behavior, investigate root causes rather than applying workarounds
- If something seems wrong, it probably is — dig deeper
## Explore vs. Implement
- **Explore first**: Before making changes, understand the existing code structure and patterns
- **Read before writing**: Always read relevant files before modifying them
- **Follow existing patterns**: Match the code style and architecture already in use
- **Minimal changes**: Only change what's necessary to accomplish the task
## Sub-Agent Delegation
When using Task tool to spawn sub-agents:
- Provide complete context — sub-agents don't share your conversation history unless noted
- Be specific about what you need: research only, or research + implementation
- Use `Explore` agent type for codebase investigation
- Use `Bash` agent type for running builds and tests
- Parallelize independent sub-agent tasks for efficiency
## Common Pitfalls
- Don't create new files when editing existing ones would suffice
- Don't add features beyond what was requested
- Don't refactor surrounding code when fixing a bug
- Don't add comments or documentation to code you didn't change
- Don't guess at file paths — use Glob/Grep to find them
+84
View File
@@ -0,0 +1,84 @@
---
description: Architecture overview, module structure, key components, data flow, and dependencies
globs:
- "app/**/*.kt"
- "app-common/**/*.kt"
- "**/*.gradle.kts"
---
# Architecture
## Multi-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app and Wear OS app
## Core Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Kotlin coroutines for async operations
- **Repository Pattern**: Data layer abstraction for monitoring and settings
## Key Components
### PodMonitor System
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
- `MonitorControl`: Manages MonitorService lifecycle
- `MonitorService`: Foreground service that continuously scans for AirPods
- `BluetoothEventReceiver`: Handles system Bluetooth events
### Reaction System
- `ReactionSettingsFragment`: Configuration for popup notifications
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpPodViewFactory`: Creates UI components for different pod models
### Common Utilities
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
## Build Configuration
### Flavors
- **FOSS**: Open-source version without Google Play dependencies
- **Google Play (gplay)**: Version with billing client for in-app purchases
### Build Types
- **debug**: Unobfuscated, full logging, no minification
- **beta**: Obfuscated, production-ready with strict lint checks
- **release**: Fully optimized for production distribution
## Data Flow
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorService` scans for AirPods beacon data
3. `PodMonitor` processes and stores device information
4. ViewModels observe monitor data via repositories
5. UI components react to ViewModel state changes
6. `ReactionSystem` triggers popups and notifications
## Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorService` which runs as a foreground service.
## Multi-Platform Considerations
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms.
## Testing Strategy
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
## Key Dependencies
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **Moshi**: JSON serialization for configuration and debugging
- **Material Design**: UI components following Material Design guidelines
+71
View File
@@ -0,0 +1,71 @@
---
description: Build, test, lint, and release commands for Gradle
globs:
- "**/*.gradle.kts"
- "**/*.gradle"
- "gradle/**"
---
# Build Commands
## Build
```bash
# Build debug version
./gradlew assembleDebug
# Build all variants (FOSS and Google Play flavors)
./gradlew assemble
# Build specific flavor and type
./gradlew assembleFossDebug
./gradlew assembleGplayRelease
# Build app bundles for Play Store
./gradlew bundleGplayRelease
```
## Testing
```bash
# Run all unit tests
./gradlew test
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
```
## Code Quality
```bash
# Run lint for all variants
./gradlew lint
# Run lint for specific variant
./gradlew lintFossDebug
# Auto-fix lint issues where possible
./gradlew lintFix
# Update lint baseline
./gradlew updateLintBaseline
```
## Release
```bash
./gradlew assembleFossRelease assembleGplayRelease
```
## Notes
- Use `assembleFossDebug` as the default quick-check build (fastest variant)
- Run `./gradlew check` before submitting changes to catch lint and test issues
- Instrumentation tests require a connected device or running emulator
+41
View File
@@ -0,0 +1,41 @@
---
description: Git commit message format and conventions
globs:
- "**"
---
# Commit Guidelines
## Format
```
<prefix>: <Short summary>
```
Summary line should be concise and describe the change. No period at the end.
## Prefixes
Use the existing commit history as reference. Common prefixes:
- **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`)
- **feat**: New features
- **refactor**: Code restructuring without behavior change
- **chore**: Maintenance, dependency updates, build config
- **docs**: Documentation changes
## Component Scope (optional)
When a change is scoped to a specific area, include it after the prefix:
- `fix(widget): Fix layout for devices with single charge detection`
- `feat(monitor): Add battery level caching`
- `refactor(popup): Extract pod view factory`
## Rules
- Keep the summary line under 72 characters
- Use imperative mood ("Add feature" not "Added feature")
- Reference issue numbers when applicable
- Do not include `Co-authored-by` trailers
- Look at recent `git log` output to match the project's existing style
+21
View File
@@ -0,0 +1,21 @@
---
description: Guidelines for adding and naming Android string resources
globs:
- "**/res/values*/strings.xml"
---
# Localization Guidelines
When adding new user-facing strings:
- **Always use string resources**: Never hardcode user-facing text in layouts or code
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
- **Provide context**: String names should indicate usage and location
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
## Naming Examples
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)
+31
View File
@@ -0,0 +1,31 @@
{
"permissions": {
"allow": [
"mcp__ide__getDiagnostics",
"Bash(./gradlew tasks:*)",
"Bash(./gradlew:*)",
"Bash(find:*)",
"Bash(ls:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(git add:*)",
"Bash(git log:*)",
"Bash(git commit:*)",
"Bash(git show:*)",
"Bash(git checkout:*)",
"Bash(git branch:*)",
"Bash(git stash:*)",
"Bash(gh issue list:*)",
"Bash(gh pr list:*)",
"Bash(gh label list:*)",
"WebSearch",
"WebFetch(domain:support.google.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:mvnrepository.com)",
"WebFetch(domain:kotlinlang.org)",
"WebFetch(domain:developer.android.com)",
"WebFetch(domain:issuetracker.google.com)"
],
"deny": []
}
}
+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:
- "*"
+48 -21
View File
@@ -7,28 +7,55 @@ on:
branches: [ main ]
jobs:
build-and-test:
name: Build and test
runs-on: ubuntu-latest
lint-vital:
name: Lint vitals
strategy:
fail-fast: false
matrix:
flavor: [ Foss,Gplay ]
variant: [ Beta,Release ]
module: [ app ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: gradle
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Setup project and build environment
uses: ./.github/actions/common-setup
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Check for fatal lint issues
run: ./gradlew lintVital${{ matrix.flavor }}${{ matrix.variant }}
- name: Build FOSS variant
run: ./gradlew assembleFossDebug
- name: Test FOSS variant
run: ./gradlew testFossDebugUnitTest
build-modules:
name: Build apps
strategy:
fail-fast: false
matrix:
flavor: [ Foss,Gplay ]
variant: [ Debug ]
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: Build Google Play variant
run: ./gradlew assembleGplayDebug
- name: Test Google Play variant
run: ./gradlew testGplayDebugUnitTest
- name: Build modules
run: ./gradlew ${{ matrix.module }}:assemble${{ matrix.flavor }}${{ matrix.variant }}
test-modules:
name: Unit tests
strategy:
fail-fast: false
matrix:
variant: [ Debug ]
flavor: [ testFoss,testGplay ]
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: 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
+23 -35
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: 11
- name: Assemble beta APK
if: contains(steps.tagger.outputs.tag, '-beta')
run: ./gradlew assembleFossBeta
@@ -48,7 +47,6 @@ jobs:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
- name: Assemble production APK
if: "!contains(steps.tagger.outputs.tag, '-beta')"
@@ -58,39 +56,34 @@ jobs:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
- 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 }}
name: ${{ steps.tagger.outputs.tag }}
generate_release_notes: true
files: |
app/build/outputs/apk/foss/beta/*.apk
app-wear/build/outputs/apk/foss/beta/*.apk
files: app/build/outputs/apk/foss/beta/eu.darken.capod-*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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/eu.darken.capod-*.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
@@ -113,29 +106,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: 11
- 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 beta and upload to Google Play
@@ -145,7 +135,6 @@ jobs:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
- name: Assemble production and upload to Google Play
if: "!contains(steps.tagger.outputs.tag, '-beta')"
@@ -153,5 +142,4 @@ jobs:
env:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
+3
View File
@@ -12,3 +12,6 @@
*.jks
.local/*
/fastlane/report.xml
/fastlane/Appfile
/fastlane/README.md
.kotlin
+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
+97 -86
View File
@@ -1,43 +1,48 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.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.610.0)
aws-sdk-core (3.131.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
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.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.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.4)
aws-sigv4 (1.5.1)
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.4)
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.7.6)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.92.4)
faraday (1.10.0)
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.6)
fastlane (2.208.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)
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.25.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.0)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
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,99 +126,100 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.18.0)
google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
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.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.2.0)
google-cloud-storage (1.37.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.2.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)
jmespath (1.6.1)
json (2.6.2)
jwt (2.4.1)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
httpclient (2.9.0)
mutex_m
jmespath (1.6.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.0.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.6.0)
public_suffix (4.0.7)
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)
multi_json (~> 1.10)
simctl (1.6.8)
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.7.0)
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
+35 -9
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.
@@ -20,23 +26,43 @@ On Android 12+ the newer and more fine grained `BLUETOOTH_SCAN` permission is us
Bluetooth Low Energy is a technology that devices like AirPods use to communicate their status to nearby devices.
CAPod requests location permissions because these permissions are required to work with Bluetooth Low Energy data.
This is a privacy measure on Android's side because you could determine someones location by scanning for Bluetooth devices:
If you know the physical location of a Bluetooth device (e.g. AirTags) you could use Bluetooth data to calculate your position.
This is a privacy measure on Android's side because you could determine someones location by scanning for Bluetooth
devices:
If you know the physical location of a Bluetooth device (e.g. AirTags) you could use Bluetooth data to calculate your
position.
### Location access in the background
CAPod uses the "location access in the background" permission (`ACCESS_BACKGROUND_LOCATION`) on Android 11 and older to receive Bluetooth Low Energy data while the app is in the background. This permission enables the "Show popup" and "Autoconnect" features and allows CAPod to react to nearby devices whil the app is closed.
CAPod uses the "location access in the background" permission (`ACCESS_BACKGROUND_LOCATION`) on Android 11 and older to
receive Bluetooth Low Energy data while the app is in the background. This permission enables the "Show popup" and "
Autoconnect" features and allows CAPod to react to nearby devices whil the app is closed.
## Automatic crash reports
## Automatic error reports
Anonymous device information may be collected in the event of an app crash or error.
*This was removed in v2.11.0+*
To do this the app uses the service "Bugsnag":
If an error occurs, an automated report may be sent to help me fix the issue.
This is optional and you can opt out of this in the settings.
Error reports are collected using "Bugsnag":
https://www.bugsnag.com/
Bugsnag's privacy policy can be found here:
https://docs.bugsnag.com/legal/privacy-policy/
Crash reports may contain device and app related information, e.g. your phone model, Android version and app version.
Error reports contain device and app information related to the error that occured.
Additional information about the error context may also be included, e.g. what this app did shortly before the error.
You can disable automatic reports in the app's settings.
Additional details about the type of data that is collected by the error tracking SDK can be found here:
https://docs.bugsnag.com/platforms/android/playstore-privacy/
Error reports are pseudonymous. Unless you tell me your install-ID, I don't know that an error report came from you.
Error reports are automatically deleted after 90 days.
## Debug logs
The app has a debug log feature that can be used to assist troubleshooting efforts. This feature creates a log file that contains verbose output of what the app is doing.
It is manually triggered by the user through an option in the app settings. The recorded log file can be shared through compatible apps (e.g. your email app) using the system's share dialog. As this log file may contain sensitive information (e.g. details about files or installed applications) it should only be shared with trusted parties.
+26 -9
View File
@@ -1,10 +1,12 @@
<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)
[![Github All Releases](https://img.shields.io/github/downloads/d4rken-org/capod/total.svg)](https://github.com/d4rken-org/capod/releases/latest)
[![Github Downloads](https://img.shields.io/github/downloads/d4rken-org/capod/total.svg?label=GitHub%20Downloads&logo=github)](https://github.com/d4rken-org/capod/edit/main/README.md#download)
[![Google Play Downloads](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24totalinstalls)](https://github.com/d4rken-org/capod/edit/main/README.md#download)
A companion app that adds support for AirPod specific features to Android:
@@ -15,7 +17,6 @@ A companion app that adds support for AirPod specific features to Android:
* Ear detection with automatic play/pause.
* Automatically connect phone & AirPods.
* Show popup when case is opened.
* Support for Wear OS
* Widgets
CAPod is ad-free. Some additional features require an in-app purchase.
@@ -25,25 +26,36 @@ 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)
* AirPods Pro 3. Generation
* AirPods Max
* Power Beats Pro
* Power Beats 3
* Power Beats 4
* Beats Solo 3
* Beats Studio 3
* Beats X
* Beats Flex
* Beats Fit Pro
* Fake AirPods 1. Generation 🎭
* Fake AirPods 2. Generation 🎭
* Fake AirPods 3. Generation 🎭
* Fake AirPods Pro1 🎭
* Fake AirPods Pro2 🎭
## Download
| Source | Status |
|-----------------------|--------|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [![](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplayshields.herokuapp.com%2Fplay%3Fi%3Deu.darken.capod%26l%3DAndroid%26m%3D%24version)](https://play.google.com/store/apps/details?id=eu.darken.capod) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | ? |
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [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/) |
| Source | Status |
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [![](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24version)](https://play.google.com/store/apps/details?id=eu.darken.capod) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | |
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [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
@@ -60,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,
@@ -68,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
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+1 -1
View File
@@ -1 +1 @@
2.3.1-rc0 20301000
3.0.4-rc1 30004010
+28
View File
@@ -0,0 +1,28 @@
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
- README.md
- CHANGELOG.md
exclude:
- buildSrc
- gradle/wrapper
- fastlane
- Gemfile
- Gemfile.lock
- crowdin*
- app
- app-common
- CONTRIBUTING.md
+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>
-1
View File
@@ -1 +0,0 @@
/build
-100
View File
@@ -1,100 +0,0 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-parcelize")
}
apply(plugin = "dagger.hilt.android.plugin")
android {
compileSdk = ProjectConfig.compileSdk
defaultConfig {
minSdk = ProjectConfig.minSdk
targetSdk = ProjectConfig.targetSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
buildConfigField("Long", "VERSION_CODE", "${ProjectConfig.Version.code}L")
buildConfigField("String", "VERSION_NAME", "\"${ProjectConfig.Version.name}\"")
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
}
buildFeatures {
viewBinding = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlin.time.ExperimentalTime",
"-opt-in=kotlin.ExperimentalUnsignedTypes",
)
}
flavorDimensions.add("version")
productFlavors {
create("foss") {
dimension = "version"
}
create("gplay") {
dimension = "version"
}
}
buildTypes {
val customProguardRules = fileTree(File("../proguard")) {
include("*.pro")
}
debug {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
proguardFiles("proguard-rules-debug.pro")
}
create("beta") {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
}
release {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
}
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
addBaseAndroid()
addBaseAndroidUi()
addBaseKotlin()
addDagger()
addMoshi()
addBaseWorkManager()
addNavigation()
addTesting()
implementation("com.bugsnag:bugsnag-android:5.9.2")
}
View File
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.darken.capod.common">
</manifest>
@@ -1,45 +0,0 @@
package eu.darken.capod.common
// Can't be const because that prevents them from being mocked in tests
@Suppress("MayBeConstant")
object BuildConfigWrap {
val DEBUG: Boolean = BuildConfig.DEBUG
val BUILD_TYPE: BuildType = when (val typ = BuildConfig.BUILD_TYPE) {
"debug" -> BuildType.DEV
"beta" -> BuildType.BETA
"release" -> BuildType.RELEASE
else -> throw IllegalArgumentException("Unknown buildtype: $typ")
}
enum class BuildType {
DEV,
BETA,
RELEASE,
;
}
val FLAVOR: Flavor = when (val flav = BuildConfig.FLAVOR) {
"gplay" -> Flavor.GPLAY
"foss" -> Flavor.FOSS
else -> throw IllegalStateException("Unknown flavor: $flav")
}
enum class Flavor {
GPLAY,
FOSS,
;
}
val APPLICATION_ID: String = BuildConfig.APPLICATION_ID
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
val VERSION_NAME: String = BuildConfig.VERSION_NAME
val GIT_SHA: String = BuildConfig.GITSHA
val BUILDTIME: String = BuildConfig.BUILDTIME
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME [$GIT_SHA] $FLAVOR"
val VERSION_DESCRIPTION_TINY: String = "v$VERSION_NAME"
}
@@ -1,5 +0,0 @@
package eu.darken.capod.common
object PrivacyPolicy {
const val URL = "https://github.com/d4rken-org/capod/blob/main/PRIVACY_POLICY.md"
}
@@ -1,151 +0,0 @@
package eu.darken.capod.common.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BleScanner @Inject constructor(
@ApplicationContext private val context: Context,
private val bluetoothManager: BluetoothManager2,
private val fakeBleData: FakeBleData,
) {
@SuppressLint("MissingPermission") fun scan(
filters: Set<ScanFilter>,
scannerMode: ScannerMode,
compatMode: Boolean,
): Flow<List<BleScanResult>> = callbackFlow {
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable")
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
} && !compatMode
val supportsOffloadBatching = adapter.isOffloadedScanBatchingSupported.also {
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
} && !compatMode
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
val callback = object : ScanCallback() {
var lastScanAt = System.currentTimeMillis()
override fun onScanResult(callbackType: Int, result: ScanResult) {
log(TAG, VERBOSE) {
val delay = System.currentTimeMillis() - lastScanAt
lastScanAt = System.currentTimeMillis()
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
}
val toSend = if (
supportsOffloadFiltering
|| filters.isEmpty()
|| filters.any { it.matchesSafe(result) }
) {
listOf(BleScanResult.fromScanResult(result))
} else {
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
emptyList()
}
trySend(toSend)
}
override fun onBatchScanResults(results: MutableList<ScanResult>) {
log(TAG, VERBOSE) {
val delay = System.currentTimeMillis() - lastScanAt
lastScanAt = System.currentTimeMillis()
"onBatchScanResults(delay=${delay}ms, results=$results)"
}
val toSend = results
.filter { result ->
val passed = when {
supportsOffloadFiltering -> true
filters.isEmpty() -> true
else -> filters.any { it.matches(result) }
}
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
passed
}
.map { BleScanResult.fromScanResult(it) }
trySend(toSend)
}
override fun onScanFailed(errorCode: Int) {
log(TAG, WARN) { "onScanFailed(errorCode=$errorCode)" }
}
}
val settings = ScanSettings.Builder().apply {
setScanMode(
when (scannerMode) {
ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER
ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
}
)
if (supportsOffloadBatching) {
setReportDelay(
when (scannerMode) {
ScannerMode.LOW_POWER -> 2000L
ScannerMode.BALANCED -> 1000L
ScannerMode.LOW_LATENCY -> 500L
}
)
}
}.build()
log(TAG, VERBOSE) { "Settings created for offloaded filtering: $settings" }
val flushJob = launch {
log(TAG) { "Flush job launched" }
while (isActive) {
// Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12
log(TAG, VERBOSE) { "Flushing scan results." }
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
when (scannerMode) {
ScannerMode.LOW_POWER -> break
ScannerMode.BALANCED -> delay(1000)
ScannerMode.LOW_LATENCY -> delay(500)
}
}
}
scanner.startScan(
if (supportsOffloadFiltering) filters.toList() else listOf(ScanFilter.Builder().build()),
settings,
callback
)
log(TAG) { "BleScanner started (filters=$filters, settings=$settings)" }
awaitClose {
flushJob.cancel()
scanner.stopScan(callback)
log(TAG) { "BleScanner stopped" }
}
}
.map { fakeBleData.maybeAddfakeData(it) }
companion object {
private val TAG = logTag("Bluetooth", "BleScanner")
}
}
@@ -1,185 +0,0 @@
package eu.darken.capod.common.bluetooth
import android.bluetooth.*
import android.bluetooth.le.BluetoothLeScanner
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.os.HandlerThread
import android.os.ParcelUuid
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BluetoothManager2 @Inject constructor(
private val manager: BluetoothManager,
@ApplicationContext private val context: Context,
private val dispatcherProvider: DispatcherProvider,
) {
val adapter: BluetoothAdapter?
get() = manager.adapter
val scanner: BluetoothLeScanner?
get() = adapter?.bluetoothLeScanner
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
send(manager.adapter?.isEnabled ?: false)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (BluetoothAdapter.ACTION_STATE_CHANGED != intent.action) {
log(TAG) { "Unknown BluetoothAdapter action: $intent" }
return
}
val value = when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
BluetoothAdapter.STATE_OFF -> false
BluetoothAdapter.STATE_ON -> true
else -> false
}
trySend(value)
}
}
context.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
awaitClose { context.unregisterReceiver(receiver) }
}
fun getBluetoothProfile(profile: Int = BluetoothProfile.HEADSET): Flow<BluetoothProfile2> = callbackFlow {
log(TAG, VERBOSE) { "getBluetoothProfile(profile=$profile)" }
var profileProxy: BluetoothProfile2? = null
manager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
log(TAG, VERBOSE) { "onServiceConnected(profile=$profile, proxy=$proxy)" }
profileProxy = BluetoothProfile2(
profileType = profile,
profileProxy = proxy,
).also { trySend(it) }
}
override fun onServiceDisconnected(profile: Int) {
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
close(IOException("BluetoothProfile service disconnected (profile=$profile)"))
}
}, profile)
awaitClose {
log(TAG) { "Closing BluetoothProfile: $profileProxy" }
profileProxy?.let {
manager.adapter.closeProfileProxy(it.profileType, it.proxy)
}
}
}
private fun monitorDevicesForProfile(
profile: Int = BluetoothProfile.HEADSET
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
callbackFlow {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
trySend(bluetoothProfile.connectedDevices)
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
start()
}
val handler = Handler(handlerThread.looper)
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
val action = intent.action
if (action == null) {
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
return
}
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
if (device == null) {
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
return
}
this@callbackFlow.launch {
val currentDevices = bluetoothProfile.connectedDevices
when (action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
log(TAG) { "Adding $device to current devices $currentDevices" }
trySend(currentDevices.plus(device))
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
log(TAG) { "Removing $device from current devices $currentDevices" }
trySend(currentDevices.minus(device))
}
}
}
}
}
context.registerReceiver(receiver, filter, null, handler)
awaitClose {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
context.unregisterReceiver(receiver)
}
}
}
fun connectedDevices(
featureFilter: Set<ParcelUuid> = ContinuityProtocol.BLE_FEATURE_UUIDS
): Flow<List<BluetoothDevice>> = isBluetoothEnabled
.flatMapLatest { monitorDevicesForProfile(BluetoothProfile.HEADSET) }
.map { devices ->
devices.filter { device ->
featureFilter.any { feature ->
device.hasFeature(feature)
}
}
}
fun bondedDevices(): Flow<Set<BluetoothDevice>> = flow {
emit(adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable"))
}
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
try {
log(TAG) { "Nudging Android connection to $device" }
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
"connect", BluetoothDevice::class.java
).apply { isAccessible = true }
connectMethod.invoke(bluetoothProfile.proxy, device)
log(TAG) { "Nudged connection to $device" }
true
} catch (e: Exception) {
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
false
}
}.first()
companion object {
private val TAG = logTag("Bluetooth", "Manager2")
}
}
@@ -1,60 +0,0 @@
package eu.darken.capod.common.debug.autoreport
import android.content.Context
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.InstallId
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagErrorHandler
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagLogger
import eu.darken.capod.common.debug.autoreport.bugsnag.NOPBugsnagErrorHandler
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
@Singleton
class AutoReporting @Inject constructor(
@ApplicationContext private val context: Context,
private val debugSettings: DebugSettings,
private val installId: InstallId,
private val bugsnagLogger: Provider<BugsnagLogger>,
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
) {
fun setup() {
val isEnabled = debugSettings.isAutoReportingEnabled.value
log(TAG) { "setup(): isEnabled=$isEnabled" }
try {
val bugsnagConfig = Configuration.load(context).apply {
if (debugSettings.isAutoReportingEnabled.value) {
Logging.install(bugsnagLogger.get())
setUser(installId.id, null, null)
autoTrackSessions = true
addOnError(bugsnagErrorHandler.get())
addMetadata("App", "buildFlavor", BuildConfigWrap.FLAVOR)
log(TAG) { "Bugsnag setup done!" }
} else {
autoTrackSessions = false
addOnError(nopBugsnagErrorHandler.get())
log(TAG) { "Installing Bugsnag NOP error handler due to user opt-out!" }
}
}
Bugsnag.start(context, bugsnagConfig)
Bugs.ready = true
} catch (e: IllegalStateException) {
log(TAG) { "Bugsnag API Key not configured." }
}
}
companion object {
private val TAG = logTag("Debug", "AutoReport")
}
}
@@ -1,58 +0,0 @@
package eu.darken.capod.common.debug.autoreport.bugsnag
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.debug.autoreport.DebugSettings
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 javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BugsnagErrorHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val bugsnagLogger: BugsnagLogger,
private val debugSettings: DebugSettings,
) : OnErrorCallback {
override fun onError(event: Event): Boolean {
bugsnagLogger.injectLog(event)
TAB_APP.also { tab ->
event.addMetadata(tab, "gitSha", BuildConfigWrap.GIT_SHA)
event.addMetadata(tab, "buildTime", BuildConfigWrap.BUILDTIME)
context.tryFormattedSignature()?.let { event.addMetadata(tab, "signatures", it) }
}
return debugSettings.isAutoReportingEnabled.value && !eu.darken.capod.common.BuildConfigWrap.DEBUG
}
companion object {
private const val TAB_APP = "app"
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
fun Context.tryFormattedSignature(): String? = try {
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures?.let { sigs ->
val sb = StringBuilder("[")
for (i in sigs.indices) {
sb.append(sigs[i].hashCode())
if (i + 1 != sigs.size) sb.append(", ")
}
sb.append("]")
sb.toString()
}
} catch (e: Exception) {
log(WARN) { e.asLog() }
null
}
}
}
@@ -1,47 +0,0 @@
package eu.darken.capod.common.debug.autoreport.bugsnag
import com.bugsnag.android.Event
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.asLog
import java.lang.String.format
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BugsnagLogger @Inject constructor() : Logging.Logger {
// Adding one to the initial size accounts for the add before remove.
private val buffer: Deque<String> = ArrayDeque(BUFFER_SIZE + 1)
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
val line = "${System.currentTimeMillis()} ${priority.toLabel()}/$tag: $message"
synchronized(buffer) {
buffer.addLast(line)
if (buffer.size > BUFFER_SIZE) {
buffer.removeFirst()
}
}
}
fun injectLog(event: Event) {
synchronized(buffer) {
var i = 100
buffer.forEach { event.addMetadata("Log", format(Locale.ROOT, "%03d", i++), it) }
event.addMetadata("Log", format(Locale.ROOT, "%03d", i), event.originalError?.asLog())
}
}
companion object {
private const val BUFFER_SIZE = 200
private fun Logging.Priority.toLabel(): String = when (this) {
Logging.Priority.VERBOSE -> "V"
Logging.Priority.DEBUG -> "D"
Logging.Priority.INFO -> "I"
Logging.Priority.WARN -> "W"
Logging.Priority.ERROR -> "E"
Logging.Priority.ASSERT -> "WTF"
}
}
}
@@ -1,19 +0,0 @@
package eu.darken.capod.common.debug.autoreport.bugsnag
import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback
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 javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NOPBugsnagErrorHandler @Inject constructor() : OnErrorCallback {
override fun onError(event: Event): Boolean {
log(WARN) { "Error, but skipping bugsnag due to user opt-out: ${event.originalError?.asLog()}" }
return false
}
}
@@ -1,20 +0,0 @@
package eu.darken.capod.common.navigation
import android.os.Bundle
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavArgs
import androidx.navigation.NavArgsLazy
import java.io.Serializable
// TODO Remove with "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha/stable"
inline fun <reified Args : NavArgs> SavedStateHandle.navArgs() = NavArgsLazy(Args::class) {
Bundle().apply {
keys().forEach {
when (val value = get<Any>(it)) {
is Serializable -> putSerializable(it, value)
is Parcelable -> putParcelable(it, value)
}
}
}
}
@@ -1,73 +0,0 @@
package eu.darken.capod.main.core
import android.content.Context
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.ScannerMode
import eu.darken.capod.common.debug.autoreport.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 javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GeneralSettings @Inject constructor(
@ApplicationContext private val context: Context,
debugSettings: DebugSettings,
moshi: Moshi,
) : Settings() {
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
val monitorMode = preferences.createFlowPreference(
"core.monitor.mode",
MonitorMode.AUTOMATIC,
moshi
)
val scannerMode = preferences.createFlowPreference(
"core.scanner.mode",
ScannerMode.LOW_LATENCY,
moshi
)
val compatibilityMode = preferences.createFlowPreference(
"core.compatibility.enabled",
false
)
val showAll = preferences.createFlowPreference(
"core.showall.enabled",
false
)
val minimumSignalQuality = preferences.createFlowPreference(
"core.signal.minimum",
0.25f
)
val mainDeviceAddress = preferences.createFlowPreference<String?>(
"core.maindevice.address",
null
)
val mainDeviceModel = preferences.createFlowPreference<PodDevice.Model>(
"core.maindevice.model",
PodDevice.Model.UNKNOWN,
moshi
)
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
monitorMode,
scannerMode,
compatibilityMode,
showAll,
minimumSignalQuality,
mainDeviceAddress,
debugSettings.isAutoReportingEnabled,
)
}
@@ -1,18 +0,0 @@
package eu.darken.capod.monitor.core
import dagger.BindsInstance
import dagger.hilt.DefineComponent
import dagger.hilt.components.SingletonComponent
@MonitorScope
@DefineComponent(parent = SingletonComponent::class)
interface MonitorComponent {
@DefineComponent.Builder
interface Builder {
fun coroutineScope(@BindsInstance coroutineScope: MonitorCoroutineScope): Builder
fun build(): MonitorComponent
}
}
@@ -1,16 +0,0 @@
package eu.darken.capod.monitor.core
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import kotlinx.coroutines.CoroutineScope
@InstallIn(MonitorComponent::class)
@Module()
abstract class ProcessorModule {
@Binds
@MonitorScope
abstract fun processorScope(scope: MonitorCoroutineScope): CoroutineScope
}
@@ -1,8 +0,0 @@
package eu.darken.capod.monitor.core
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class MonitorScope
@@ -1,13 +0,0 @@
package eu.darken.capod.pods.core
interface DualPodDevice : PodDevice {
enum class Pod {
LEFT,
RIGHT
}
val batteryLeftPodPercent: Float?
val batteryRightPodPercent: Float?
}
@@ -1,9 +0,0 @@
package eu.darken.capod.pods.core
interface HasCase {
val batteryCasePercent: Float?
val isCaseCharging: Boolean
}
@@ -1,121 +0,0 @@
package eu.darken.capod.pods.core
import android.content.Context
import androidx.annotation.DrawableRes
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult
import java.time.Instant
import java.util.*
import kotlin.math.abs
import kotlin.math.max
interface PodDevice {
val identifier: Id
val model: Model
val address: String
get() = scanResult.address
val seenLastAt: Instant
val seenFirstAt: Instant
val seenCounter: Int
val scanResult: BleScanResult
val rssi: Int
get() = scanResult.rssi
val confidence: Float
/**
* This is not correct but it works ¯\_(ツ)_/¯
* The range of the RSSI is device specific (ROMs).
*/
val signalQuality: Float
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
val rawData: Map<Int, ByteArray>
get() = scanResult.manufacturerSpecificData
val rawDataHex: List<String>
get() = rawData.entries.map { entry ->
"${entry.key}: ${entry.value.joinToString(separator = " ") { String.format("%02X", it) }}"
}
fun getLabel(context: Context): String = model.label
@get:DrawableRes
val iconRes: Int
get() = model.iconRes
@JvmInline
value class Id(private val id: UUID = UUID.randomUUID())
@JsonClass(generateAdapter = false)
enum class Model(
val label: String,
@DrawableRes val iconRes: Int = R.drawable.ic_device_generic_earbuds,
) {
@Json(name = "airpods.gen1") AIRPODS_GEN1(
label = "AirPods (Gen 1)",
iconRes = R.drawable.ic_device_airpods_gen1,
),
@Json(name = "airpods.gen2") AIRPODS_GEN2(
"AirPods (Gen 2)",
R.drawable.ic_device_airpods_gen2,
),
@Json(name = "airpods.gen3") AIRPODS_GEN3(
"AirPods (Gen 3)",
R.drawable.ic_device_airpods_gen2,
),
@Json(name = "airpods.pro") AIRPODS_PRO(
"AirPods Pro",
R.drawable.ic_device_airpods_gen2
),
@Json(name = "airpods.pro2") AIRPODS_PRO2(
"AirPods Pro 2",
R.drawable.ic_device_airpods_gen2
),
@Json(name = "airpods.max") AIRPODS_MAX(
"AirPods Max",
R.drawable.ic_device_generic_headphones
),
@Json(name = "beats.flex") BEATS_FLEX(
"Beats Flex"
),
@Json(name = "beats.solo.3") BEATS_SOLO_3(
"Beats Solo 3"
),
@Json(name = "beats.studio.3") BEATS_STUDIO_3(
"Beats Studio 3"
),
@Json(name = "beats.x") BEATS_X(
"Beats X"
),
@Json(name = "beats.powerbeats.3") POWERBEATS_3(
"Power Beats 3"
),
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
"Power Beats Pro"
),
@Json(name = "fakes.tws.i99999") TWS_I99999(
"TWS i99999"
),
@Json(name = "fakes.varunr.airpodspro") VARUNR_AIRPODS_PRO(
"Fake AirPods Pro"
),
@Json(name = "unknown") UNKNOWN(
"Unknown"
);
}
companion object {
const val BASE_CONFIDENCE = 0.5f
}
}
@@ -1,66 +0,0 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.bluetooth.BleScanResult
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.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.ProximityPairing
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
import javax.inject.Singleton
@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 unknownAppleFactory: UnknownAppleDevice.Factory,
) {
private val lock = Mutex()
private fun getMessage(scanResult: BleScanResult): ProximityPairing.Message? {
val messages = try {
continuityProtocolDecoder.decode(scanResult)
} catch (e: Exception) {
log(TAG, WARN) { "Data wasn't continuity protocol conform:\n${e.asLog()}" }
return null
}
if (messages.isEmpty()) {
log(TAG, WARN) { "Data contained no continuity messages: $scanResult" }
return null
}
if (messages.size > 1) {
log(TAG, WARN) { "Decoded multiple continuity messages, picking first: $messages" }
}
val proximityMessage = proximityPairingDecoder.decode(messages.first())
if (proximityMessage == null) {
log(TAG) { "Not a proximity pairing message: $messages" }
return null
}
return proximityMessage
}
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
val pm = getMessage(scanResult) ?: return@withLock null
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
return@withLock (factory ?: unknownAppleFactory).create(
scanResult = scanResult,
message = pm,
)
}
companion object {
private val TAG = logTag("Pod", "Apple", "Factory")
}
}
@@ -1,35 +0,0 @@
package eu.darken.capod.pods.core.apple
import dagger.Binds
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.Twsi99999
import eu.darken.capod.pods.core.apple.misc.VarunrAirPodsPro
@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 airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
@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 powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet
abstract fun fakesVarunrAirPodsPro(factory: VarunrAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
}
@@ -1,48 +0,0 @@
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
interface ApplePods : PodDevice {
val proximityMessage: ProximityPairing.Message
// We start counting at the airpods prefix byte
val rawPrefix: UByte
get() = proximityMessage.data[0]
val rawDeviceModel: UShort
get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort()
val rawStatus: UByte
get() = proximityMessage.data[3]
val rawStatusHex: String
get() = rawStatus.toHex()
val rawPodsBattery: UByte
get() = proximityMessage.data[4]
val rawPodsBatteryHex: String
get() = rawPodsBattery.toHex()
val rawFlags: UShort
get() = proximityMessage.data[5].upperNibble
val rawCaseBattery: UShort
get() = proximityMessage.data[5].lowerNibble
val rawCaseLidState: UByte
get() = proximityMessage.data[6]
val rawDeviceColor: UByte
get() = proximityMessage.data[7]
val rawSuffix: UByte
get() = proximityMessage.data[8]
}
@@ -1,182 +0,0 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.bluetooth.BleScanResult
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.airpods.AirPodsPro
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Duration
import java.time.Instant
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
data class Markings(
val vendor: UByte,
val length: UByte,
val device: UShort,
val podBatteryData: Set<UShort>,
val caseBatteryData: UShort,
val deviceColor: 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]
)
data class KnownDevice(
val id: PodDevice.Id,
val seenFirstAt: Instant,
val seenCounter: Int,
val history: List<ApplePods>
) {
val lastMessage: ProximityPairing.Message
get() = history.last().proximityMessage
val lastAddress: String
get() = history.last().address
val confidence: Float
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
fun averageRssi(latest: Int): Int =
history.map { it.rssi }.plus(latest).takeLast(10).median()
fun isOlderThan(age: Duration): Boolean {
val now = Instant.now()
return Duration.between(history.last().seenLastAt, now) > age
}
private fun List<Int>.median(): Int = this.sorted().let {
if (it.size % 2 == 0)
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
else
it[it.size / 2]
}
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
companion object {
const val MAX_HISTORY = 20
}
}
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
fun KnownDevice.getLatestCaseBattery(): Float? = history
.filterIsInstance<HasCase>()
.mapNotNull { it.batteryCasePercent }
.lastOrNull()
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? {
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED)
if (definitive.contains(basic.caseLidState)) return null
return history
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
?.caseLidState
}
open fun historyTrimmer(
pods: List<ApplePods>
): List<ApplePods> {
var trimmed = pods.takeLast(KnownDevice.MAX_HISTORY)
// We want to keep the case information
// If both pods are removed, and produce more history, we otherwise lose the case battery info.
val caseInfoFat = pods.lastOrNull { it is HasCase && it.batteryCasePercent != null }
val caseInfoTrimmed = trimmed.lastOrNull { it is HasCase && it.batteryCasePercent != null }
if (caseInfoFat != null && caseInfoTrimmed == null) {
trimmed = listOf(caseInfoFat) + trimmed.takeLast(KnownDevice.MAX_HISTORY - 1)
}
return trimmed
}
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(20))) {
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 -> {
existing.copy(
seenCounter = existing.seenCounter + 1,
history = existing.history.plus(device)
)
}
else -> {
log(tag) { "searchHistory1: Creating new history for $device" }
KnownDevice(
id = device.identifier,
seenFirstAt = device.seenFirstAt,
seenCounter = 1,
history = listOf(device)
)
}
}
}
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
}
@@ -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<DualAirPods>(tag) {
fun DualAirPods.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: DualAirPods): Collection<KnownDevice> {
val target = device.getCaseMatchMarkings()
return filter { known ->
known.history
.filterIsInstance<DualAirPods>()
.any { it.getCaseMatchMarkings() == target }
}
}
override fun searchHistory(current: DualAirPods): 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
}
}
}
}
@@ -1,22 +0,0 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.pods.core.SinglePodDevice
/**
* Devices that only present a single charge level, e.g. most Beats devices
*/
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)
}
}
}
@@ -1,3 +0,0 @@
package eu.darken.capod.pods.core.apple
abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory<SingleApplePods>(tag)
@@ -1,70 +0,0 @@
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.HasEarDetection
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
data class AirPodsMax(
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 confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasEarDetection {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
override val rssi: Int
get() = rssiAverage ?: super.rssi
val isHeadphonesBeingWorn: Boolean
get() = rawStatus.isBitSet(1)
val isHeadsetBeingCharged: Boolean
get() = rawFlags.isBitSet(0)
override val isBeingWorn: Boolean
get() = isHeadphonesBeingWorn
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
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE_DIRTY = 10.toUByte()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max")
}
}
@@ -1,144 +0,0 @@
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.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasDualMicrophone
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 java.time.Instant
import javax.inject.Inject
/**
* Basically an AirPods GEN1 clone
* Similar data structure but a lot of placeholder values or hardcoded values
*/
data class Twsi99999 constructor(
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 confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
override val model: PodDevice.Model = PodDevice.Model.TWS_I99999
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
}
}
}
val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
/**
* The data flip bit is set if the left pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isLeftPodMicrophone: Boolean
get() = rawStatus.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
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
}
}
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor() : ApplePodsFactory<Twsi99999>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): 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 = Twsi99999(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x0220.toUShort()
private val TAG = logTag("PodDevice", "Apple", "TWS", "i99999")
}
}
@@ -1,144 +0,0 @@
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.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasDualMicrophone
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 java.time.Instant
import javax.inject.Inject
/**
* AirPods Pro clone similar to Twsi999999.
* Shorter data structure.
*/
data class VarunrAirPodsPro constructor(
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 confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
override val model: PodDevice.Model = PodDevice.Model.VARUNR_AIRPODS_PRO
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
}
}
}
val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
/**
* The data flip bit is set if the left pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isLeftPodMicrophone: Boolean
get() = rawStatus.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
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
}
}
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor() : ApplePodsFactory<VarunrAirPodsPro>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): 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 = VarunrAirPodsPro(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x0E20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "Varunr", "AirPodsPro")
}
}
@@ -1,91 +0,0 @@
package eu.darken.capod.reaction.core.playpause
import eu.darken.capod.common.MediaControl
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.reaction.core.ReactionSettings
import kotlinx.coroutines.flow.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PlayPause @Inject constructor(
private val podMonitor: PodMonitor,
private val bluetoothManager: BluetoothManager2,
private val reactionSettings: ReactionSettings,
private val mediaControl: MediaControl,
) {
fun monitor() = combine(
reactionSettings.autoPlay.flow,
reactionSettings.autoPause.flow,
reactionSettings.onePodMode.flow,
) { play, pause, _ -> play || pause }
.flatMapLatest { if (it) bluetoothManager.connectedDevices() else emptyFlow() }
.flatMapLatest {
if (it.isEmpty()) {
log(TAG) { "No known devices connected." }
emptyFlow()
} else {
log(TAG) { "Known devices connected: $it" }
podMonitor.mainDevice
}
}
.distinctUntilChanged()
.withPrevious()
.onEach { (previous, current) ->
if (previous is HasEarDetection && current is HasEarDetection) {
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
if (previous.identifier != current.identifier) {
return@onEach
}
if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
val previousWorn = previous.isEitherPodInEar
val currentWorn = current.isEitherPodInEar
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
if (reactionSettings.autoPlay.value) {
mediaControl.sendPlay()
} else {
log(TAG) { "autoPlay is disabled" }
}
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
if (reactionSettings.autoPause.value) {
mediaControl.sendPause()
} else {
log(TAG) { "autoPause is disabled" }
}
}
} else if (previous.isBeingWorn != current.isBeingWorn) {
log(TAG) { "Ear status changed for monitored device." }
if (current.isBeingWorn && !mediaControl.isPlaying) {
if (reactionSettings.autoPlay.value) {
mediaControl.sendPlay()
} else {
log(TAG) { "autoPlay is disabled" }
}
} else if (!current.isBeingWorn && mediaControl.isPlaying) {
if (reactionSettings.autoPause.value) {
mediaControl.sendPause()
} else {
log(TAG) { "autoPause is disabled" }
}
}
}
}
}
.setupCommonEventHandlers(TAG) { "monitor" }
companion object {
private val TAG = logTag("Reaction", "PlayPause")
}
}
@@ -1,111 +0,0 @@
package eu.darken.capod.reaction.core.popup
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.reaction.core.ReactionSettings
import kotlinx.coroutines.flow.*
import java.time.Duration
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PopUpReaction @Inject constructor(
private val podMonitor: PodMonitor,
private val reactionSettings: ReactionSettings,
) {
private val coolDowns = mutableMapOf<PodDevice.Id, Instant>()
fun monitor(): Flow<Event> = reactionSettings.showPopUpOnCaseOpen.flow
.flatMapLatest { isEnabled ->
if (isEnabled) {
podMonitor.mainDevice.distinctUntilChangedBy { it?.rawDataHex }
} else {
emptyFlow()
}
}
.withPrevious()
.setupCommonEventHandlers(TAG) { "monitor" }
.mapNotNull { (previous, current) ->
if (previous !is DualAirPods? || current !is DualAirPods) {
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()) }
"previous=$prev (${previous?.caseLidState}), current=$cur (${current.caseLidState})"
}
log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" }
val isSameDeviceWithCaseNowOpen =
previous?.identifier == current.identifier && previous.caseLidState != current.caseLidState
val isNewDeviceWithJustOpenedCase =
previous?.identifier != current.identifier && previous?.caseLidState != current.caseLidState
if (!isSameDeviceWithCaseNowOpen && !isNewDeviceWithJustOpenedCase) {
return@mapNotNull null
}
log(TAG) { "Case lid status changed for monitored device." }
tryPopWindow(current)
}
private suspend fun tryPopWindow(current: DualAirPods): Event? = when {
current.caseLidState == DualAirPods.LidState.OPEN -> {
log(TAG, INFO) { "Show popup" }
val now = Instant.now()
val lastShown = coolDowns[current.identifier] ?: Instant.MIN
val sinceLastPop = Duration.between(lastShown, now)
log(TAG) { "Time since last popup: $sinceLastPop" }
if (sinceLastPop >= Duration.ofSeconds(10)) {
coolDowns[current.identifier] = Instant.now()
Event.PopupShow(device = current)
} else {
log(TAG, INFO) { "Popup is still on cooldown: $sinceLastPop" }
null
}
}
current.caseLidState != DualAirPods.LidState.OPEN -> {
when (current.caseLidState) {
DualAirPods.LidState.CLOSED -> {
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
coolDowns.remove(current.identifier)
}
else -> {
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
coolDowns[current.identifier] = Instant.now()
}
}
log(TAG, INFO) { "Hide popup" }
Event.PopupHide()
}
else -> null
}
sealed class Event {
data class PopupShow(
val eventAt: Instant = Instant.now(),
val device: PodDevice,
) : Event()
data class PopupHide(
val eventAt: Instant = Instant.now(),
) : Event()
}
companion object {
private val TAG = logTag("Reaction", "PopUp")
}
}
@@ -1,10 +0,0 @@
<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="m6,11c-1.9954,0 -3,-2.0093 -3,-4s1.0046,-4 3,-4c3.1837,0 5,3.4637 5,6.4886v10.0114c0,0.8284 -0.6716,1.5 -1.5,1.5 -0.8284,0 -1.5,-0.6716 -1.5,-1.5v-8.0113c0,-0.381 -0.1426,-0.7378 -0.3906,-1.0098 -0.4267,0.3435 -0.9676,0.5211 -1.6094,0.5211zM9,11.4887v8.0113c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-10.0114c0,-2.5454 -1.5434,-5.4886 -4,-5.4886 -1.2546,0 -2,1.4907 -2,3s0.7454,3 2,3c0.7809,0 1.2592,-0.3588 1.5257,-1.1581 0.2094,-0.6282 1.1516,-0.3212 0.9508,0.3097l-0.1878,0.5902c0.4499,0.4603 0.7114,1.0823 0.7114,1.7468zM6,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1zM16,11.4887v8.0113c0,0.8284 -0.6716,1.5 -1.5,1.5s-1.5,-0.6716 -1.5,-1.5v-10.0114c0,-3.0249 1.8163,-6.4886 5,-6.4886 1.9954,0 3,2.0093 3,4s-1.0046,4 -3,4c-0.6418,0 -1.1826,-0.1777 -1.6094,-0.5211 -0.248,0.2721 -0.3906,0.6288 -0.3906,1.0098zM15.5235,9.1516c-0.2008,-0.631 0.7414,-0.9379 0.9508,-0.3097 0.2665,0.7994 0.7448,1.1581 1.5257,1.1581 1.2546,0 2,-1.4907 2,-3s-0.7454,-3 -2,-3c-2.4566,0 -4,2.9433 -4,5.4886v10.0114c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-8.0113c0,-0.6645 0.2615,-1.2865 0.7114,-1.7468zM18,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1z" />
</vector>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_value_not_available_label">غير متوفر</string>
<string name="general_error_label">خطأ</string>
<string name="general_grant_permission_action">منح الإذن</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_access_fine_location_label">الوصول إلى موقع جيد</string>
<string name="permission_access_fine_location_description">يستخدم CAPod إذن \"الموقع الدقيق\" لتلقي بيانات البلوتوث منخفضة الطاقة. تستخدم سماعات الرأس بلوتوث منخفض الطاقة لبث حالتها. لن يستخدم هذا التطبيق بيانات البلوتوث لتحديد موقعك.</string>
<string name="permission_background_location_label">الوصول إلى الموقع في الخلفية</string>
</resources>
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<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>
<string name="overview_nomaindevice_label">Əsas cihaz yoxdur</string>
<string name="overview_nomaindevice_description">Aşkarlanan bütün cihazların sizin olma ehtimalı azdır. Cihazınızı açın, bağlantını qurun və ya tənzimləmələri edin.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth sıradan çıxarıldı</string>
<string name="overview_bluetooth_disabled_description">Bluetooth sıradan çıxarıldı, fəallaşdırın ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth bağlantısı</string>
<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_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>
<string name="permission_background_location_label">Arxaplanda yerləşmə müraciəti</string>
<string name="permission_background_location_description">CAPod, tətbiq bağlı ikən \"Açılan pəncərəni göstər\" və \"Avto-bağlantı qurma\" kimi özəllikləri fəallaşdırmaq üçün \"arxaplanda yerləşmə müraciəti\"ni istifadə edir. Arxaplanda yerləşmə müraciəti tətbiqin arxaplanda olarkən Bluetooth Zəif Enerji verilənlərini almasına icazə verir. Bu tətbiq, Bluetooth verilənlərini yerləşmənizi müəyyənləşdirmək üçün İSTİFADƏ ETMİR.</string>
<string name="permission_ignore_battery_optimizations_label">Batereya optimallaşdırmasını sıradan çıxart</string>
<string name="permission_ignore_battery_optimizations_description">Batereya optimallaşdırması, bu tətbiqin, tətbiq arxaplanda olarkən güvənli şəkildə Bluetooth verilənlərini almasına mane olur.</string>
<string name="permission_required_title">Aşağıdakı icazə verilməlidir:</string>
<string name="permission_system_alert_window_label">Sistem xəbərdarlıq pəncərəsi</string>
<string name="permission_system_alert_window_description">\"Açılan pəncərəni göstər\" özəlliyini fəal etmək üçün CAPod-a digər tətbiqlərin üzərində göstərmə icazəsi verin.</string>
<string name="settings_scanner_mode_lowpower_label">Zəif enerji</string>
<string name="settings_scanner_mode_balanced_label">Tarazlı</string>
<string name="settings_scanner_mode_lowlatency_label">Aşağı gecikmə</string>
<string name="settings_monitor_mode_manual_label">Tətbiq açılanda</string>
<string name="settings_monitor_mode_automatic_label">Cihazla bağlantı qurulanda</string>
<string name="settings_monitor_mode_always_label">Həmişə</string>
<string name="settings_reaction_autoconnect_whenseen_label">Görünəndə</string>
<string name="settings_reaction_autoconnect_caseopen_label">Qutu açıqdır</string>
<string name="settings_reaction_autoconnect_inear_label">Qulaqda</string>
<string name="pods_dual_left_label">Sol tərəf</string>
<string name="pods_dual_right_label">Sağ tərəf</string>
<string name="pods_case_label">Qutu</string>
<string name="pods_case_status_open_label"></string>
<string name="pods_case_status_closed_label">Bağlı</string>
<string name="pods_connection_state_disconnected_label">Heç bir cihazla bağlantı qurulmayıb</string>
<string name="pods_connection_state_idle_label">Bir cihazla bağlantı qurulub, ancaq boşdadır</string>
<string name="pods_connection_state_music_label">Musiqi rejimində</string>
<string name="pods_connection_state_call_label">Zəng rejimində</string>
<string name="pods_connection_state_ringing_label">Çağırır</string>
<string name="pods_connection_state_hanging_up_label">Dəstək asılır</string>
<string name="pods_connection_state_unknown_label">Bilinməyən bağlantı vəziyyəti</string>
<string name="pods_unknown_raw_data_label">Xam verilənlər</string>
<string name="pods_unknown_label">Bilinməyən cihaz</string>
<string name="pods_unknown_contact_dev">Bu bir bilinməyən cihazdır, ancaq oxşar mesaj formatı istifadə edir. Bunun üçün dəstək əlavə edək, mənimlə əlaqə saxlayın :)</string>
<string name="pods_none_label_short">Cihaz yoxdur</string>
<string name="pods_charging_label">Enerji yığır</string>
<string name="pods_inear_label">Qulaqda</string>
<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="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>
</resources>
@@ -1,61 +0,0 @@
<?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">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 для злучэння са спалучанымі прыладамі.</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 накладанне паверх іншых праграм для карэктнай работы функцыі \"Апавяшчэнні\".</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="pods_case_unknown_state">Невядомы стан</string>
<string name="last_seen_x">Апошні раз быў: %s</string>
<string name="first_seen_x">Упершыню быў: %s</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_grant_permission_action">অনুমতি প্রদান করুন</string>
</resources>
@@ -1,61 +0,0 @@
<?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">N/D</string>
<string name="general_error_label">Error</string>
<string name="general_grant_permission_action">Concedeix el permís</string>
<string name="overview_nomaindevice_label">Cap dispositiu principal</string>
<string name="overview_nomaindevice_description">És poc probable que tots els dispositius detectats siguin vostres. Enceneu i connecteu el dispositiu o ajusteu la configuració.</string>
<string name="overview_bluetooth_disabled_label">El Bluetooth està desactivat</string>
<string name="overview_bluetooth_disabled_description">El Bluetooth està desactivat, activeu-lo ;)</string>
<string name="permission_bluetooth_connect_label">Connexió Bluetooth</string>
<string name="permission_bluetooth_connect_description">Aquesta aplicació requereix el permís «Connexió Bluetooth» per interactuar amb dispositius emparellats i iniciar connexions.</string>
<string name="permission_bluetooth_scan_label">Escaneig Bluetooth</string>
<string name="permission_bluetooth_scan_description">El permís «Escaneig Bluetooth» permet que aquesta aplicació descobreixi i rebi dades Bluetooth de dispositius propers, com ara els vostres AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Aquesta aplicació requereix el permís «Bluetooth» per connectar-se als dispositius Bluetooth emparellats.</string>
<string name="permission_access_fine_location_label">Accés a la ubicació precisa</string>
<string name="permission_access_fine_location_description">El CAPod utilitza el permís «ubicació precisa» per rebre dades Bluetooth Low Energy. Els vostres auriculars utilitzen la tecnologia Bluetooth Low Energy per transmetre el seu estat. Aquesta aplicació NO utilitzarà dades Bluetooth per determinar la vostra ubicació.</string>
<string name="permission_background_location_label">Accés a la ubicació de fons</string>
<string name="permission_background_location_description">El CAPod utilitza «accés a la ubicació de fons» per activar funcions com «Mostra la finestra emergent» i «Connexió automàtica» mentre l\'aplicació està tancada. L\'accés a la ubicació de fons permet que l\'aplicació rebi dades Bluetooth Low Energy mentre està en segon pla. Aquesta aplicació NO utilitzarà dades Bluetooth per determinar la vostra ubicació.</string>
<string name="permission_ignore_battery_optimizations_label">Desactiva les optimitzacions de la bateria</string>
<string name="permission_ignore_battery_optimizations_description">Les optimitzacions de la bateria impedeixen que aquesta aplicació rebi dades Bluetooth de manera fiable mentre l\'aplicació està en segon pla.</string>
<string name="permission_required_title">Cal el permís següent:</string>
<string name="permission_system_alert_window_label">Finestra d\'alerta del sistema</string>
<string name="permission_system_alert_window_description">Permet que el CAPod dibuixi sobre altres aplicacions per fer possible la característica «Mostra emergent».</string>
<string name="settings_scanner_mode_lowpower_label">Baixa potència</string>
<string name="settings_scanner_mode_balanced_label">Equilibrat</string>
<string name="settings_scanner_mode_lowlatency_label">Baixa latència</string>
<string name="settings_monitor_mode_manual_label">Quan l\'aplicació està oberta</string>
<string name="settings_monitor_mode_automatic_label">Quan el dispositiu està connectat</string>
<string name="settings_monitor_mode_always_label">Sempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Quan es veu</string>
<string name="settings_reaction_autoconnect_caseopen_label">La funda està oberta</string>
<string name="settings_reaction_autoconnect_inear_label">A l\'orella</string>
<string name="pods_dual_left_label">AirdPod esquerre</string>
<string name="pods_dual_right_label">AirPod dret</string>
<string name="pods_case_label">Funda</string>
<string name="pods_case_status_open_label">Oberta</string>
<string name="pods_case_status_closed_label">Tancada</string>
<string name="pods_connection_state_disconnected_label">No està connectat a un dispositiu</string>
<string name="pods_connection_state_idle_label">Connectat a un dispositiu, però inactiu</string>
<string name="pods_connection_state_music_label">En mode música</string>
<string name="pods_connection_state_call_label">En mode trucada</string>
<string name="pods_connection_state_ringing_label">Sonant</string>
<string name="pods_connection_state_hanging_up_label">Penjant</string>
<string name="pods_connection_state_unknown_label">Estat de connexió desconegut</string>
<string name="pods_unknown_raw_data_label">Dades en brut</string>
<string name="pods_unknown_label">Dispositiu desconegut</string>
<string name="pods_unknown_contact_dev">Aquest és un dispositiu desconegut, però utilitza un format de missatge similar. Anem a fer-lo compatible, contacteu amb mi :)</string>
<string name="pods_none_label_short">Cap dispositiu</string>
<string name="pods_charging_label">Carregant</string>
<string name="pods_inear_label">A l\'orella</string>
<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="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>
</resources>
@@ -1,61 +0,0 @@
<?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">N/A</string>
<string name="general_error_label">Chyba</string>
<string name="general_grant_permission_action">Udělit oprávnění</string>
<string name="overview_nomaindevice_label">Žádné hlavní zařízení</string>
<string name="overview_nomaindevice_description">Je nepravděpodobné, že by všechna zjištěná zařízení patřila vám. Zapněte a připojte zařízení nebo upravte nastavení.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth je vypnuto</string>
<string name="overview_bluetooth_disabled_description">Bluetooth je vypnuto, zapněte jej ;)</string>
<string name="permission_bluetooth_connect_label">Připojení Bluetooth</string>
<string name="permission_bluetooth_connect_description">Tato aplikace vyžaduje oprávnění „připojení Bluetooth“ pro interakci se spárovanými zařízeními a zahájení připojení.</string>
<string name="permission_bluetooth_scan_label">Skenování Bluetooth</string>
<string name="permission_bluetooth_scan_description">Oprávnění „Skenování Bluetooth“ umožňuje této aplikaci zjišťovat a přijímat data z okolních Bluetooth zařízení, jako jsou například sluchátka AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Tato aplikace vyžaduje oprávněni k „Bluetooth“ pro připojení ke spárovaným zařízením Bluetooth.</string>
<string name="permission_access_fine_location_label">Přístup k přesné poloze</string>
<string name="permission_access_fine_location_description">CAPod používá pro příjem dat Bluetooth Low Energy oprávnění k „přesné poloze“. Sluchátka používají k vysílání svého stavu technologii Bluetooth Low Energy. Tato aplikace NEPOUŽÍVÁ data Bluetooth k určování vaší polohy.</string>
<string name="permission_background_location_label">Přístup k poloze na pozadí</string>
<string name="permission_background_location_description">Aplikace CAPod používá „přístup k poloze na pozadí“ pro funkce jako „Zobrazit vyskakovací okno“ a „Automatické připojení“, když je aplikace zavřená. Přístup k poloze na pozadí umožňuje aplikaci přijímat data Bluetooth Low Energy, když je na pozadí. Tato aplikace NEPOUŽÍVÁ data Bluetooth k určování vaší polohy.</string>
<string name="permission_ignore_battery_optimizations_label">Vypnout optimalizaci baterie</string>
<string name="permission_ignore_battery_optimizations_description">Optimalizace baterie zabraňuje této aplikaci spolehlivě přijímat data Bluetooth, pokud běží na pozadí.</string>
<string name="permission_required_title">Je vyžadováno následující oprávnění:</string>
<string name="permission_system_alert_window_label">Okno systémových upozornění</string>
<string name="permission_system_alert_window_description">Aby bylo možné použít unkci „Zobrazit vyskakovací okno“, je nutné umožnit aplikaci CAPod vykreslení přes jiné aplikace.</string>
<string name="settings_scanner_mode_lowpower_label">Nízký výkon</string>
<string name="settings_scanner_mode_balanced_label">Vyvážený</string>
<string name="settings_scanner_mode_lowlatency_label">Nízká latence</string>
<string name="settings_monitor_mode_manual_label">Při otevření aplikace</string>
<string name="settings_monitor_mode_automatic_label">Při připojení zařízení</string>
<string name="settings_monitor_mode_always_label">Vždy</string>
<string name="settings_reaction_autoconnect_whenseen_label">Když je vidět</string>
<string name="settings_reaction_autoconnect_caseopen_label">Pouzdro je otevřeno</string>
<string name="settings_reaction_autoconnect_inear_label">V uchu</string>
<string name="pods_dual_left_label">Levé sluchátko</string>
<string name="pods_dual_right_label">Pravé sluchátko</string>
<string name="pods_case_label">Pouzdro</string>
<string name="pods_case_status_open_label">Otevřeno</string>
<string name="pods_case_status_closed_label">Zavřeno</string>
<string name="pods_connection_state_disconnected_label">Nepřipojeno k zařízení</string>
<string name="pods_connection_state_idle_label">Připojeno k zařízení, ale bez aktivity</string>
<string name="pods_connection_state_music_label">V režimu hudby</string>
<string name="pods_connection_state_call_label">V režimu hovoru</string>
<string name="pods_connection_state_ringing_label">Vyzvánění</string>
<string name="pods_connection_state_hanging_up_label">Zavěšování</string>
<string name="pods_connection_state_unknown_label">Neznámý stav připojení</string>
<string name="pods_unknown_raw_data_label">Nezpracovaná data</string>
<string name="pods_unknown_label">Neznámé zařízení</string>
<string name="pods_unknown_contact_dev">Jedná se o neznámé zařízení, ale používá podobný formát zprávy. Přidejme pro něj podporu. Kontaktujte mě :)</string>
<string name="pods_none_label_short">Žádné zařízení</string>
<string name="pods_charging_label">Nabíjení</string>
<string name="pods_inear_label">V uchu</string>
<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="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>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,61 +0,0 @@
<?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">k.A.</string>
<string name="general_error_label">Fehler</string>
<string name="general_grant_permission_action">Berechtigung erteilen</string>
<string name="overview_nomaindevice_label">Kein primäres Gerät</string>
<string name="overview_nomaindevice_description">Alle gefundenen Geräte sind unwahrscheinlich ihnen zu gehören. Verbinden sie ihr Gerät oder passen sie die Einstellungen an.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth verbinden</string>
<string name="permission_bluetooth_connect_description">Diese App benötigt die \"Bluetooth verbinden\" Berechtigung um mit bereits verbundenen und neuen Geräten zu interagieren.</string>
<string name="permission_bluetooth_scan_label">Bluetooth Suche</string>
<string name="permission_bluetooth_scan_description">Die \"Bluetooth verbinden\" Berechtigung erlaubt dieser App Bluetooth Daten von Geräten wie AirPods zu finden und zu erhalten.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Diese App benötigt die \"Bluetooth\" Berechtigung um sich mit gekoppelten Geräten zu verbinden.</string>
<string name="permission_access_fine_location_label">Auf genauen Standort zugreifen</string>
<string name="permission_access_fine_location_description">CAPod benutzt die \"genauen Gerätestandort verwenden\" Berechtigung um Bluetooth Low Energy Daten zu erhalten. Ihre Kopfhörer nutzen Bluetooth Low Energy um ihren Batterie Status zu übertragen. Diese App greift nicht auf ihren Standort zu.</string>
<string name="permission_background_location_label">Standort Zugriff im Hintergrund</string>
<string name="permission_background_location_description">CAPod nutzt \"Standort Zugriff im Hintergrund\" um Funktionen wie \"Pop-up anzeigen\" und \"AutoConnect\" zu verwenden während die App geschlossen ist. Standort Zugriff im Hintergrund erlaubt der App Bluetooth Low Energy Daten im Hintergrund zu erhalten und NICHT ihren Standort zu finden.</string>
<string name="permission_ignore_battery_optimizations_label">Batterie Optimierung deaktivieren</string>
<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="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_reaction_autoconnect_whenseen_label">Wenn gesehen</string>
<string name="settings_reaction_autoconnect_caseopen_label">Fall 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>
<string name="pods_case_label">Ladecase</string>
<string name="pods_case_status_open_label">Offen</string>
<string name="pods_case_status_closed_label">Geschlossen</string>
<string name="pods_connection_state_disconnected_label">Mit keinem Gerät verbunden</string>
<string name="pods_connection_state_idle_label">Mit einem Gerät verbunden, aber im Leerlauf</string>
<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_unknown_label">Unbekannter Verbindungsstatus</string>
<string name="pods_unknown_raw_data_label">Rohe Daten</string>
<string name="pods_unknown_label">Unbekanntes Gerät</string>
<string name="pods_unknown_contact_dev">Dies ist ein unbekanntes Gerät, aber es verwendet ein ähnliches Nachrichtenformat. Lassen Sie uns Unterstützung dafür hinzufügen, kontaktieren Sie mich :)</string>
<string name="pods_none_label_short">Kein Gerät</string>
<string name="pods_charging_label">Laden</string>
<string name="pods_inear_label">Im Ohr</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Dein</string>
<string name="headset_being_worn_label">Wird 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>
</resources>
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<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. Τα ακουστικά σας χρησιμοποιούν τεχνολογία χαμηλής ενέργειας Bluetooth για να μεταδίδουν την κατάστασή τους. Αυτή η εφαρμογή ΔΕΝ θα χρησιμοποιεί δεδομένα Bluetooth για να προσδιορίσει την τοποθεσία σας.</string>
<string name="permission_background_location_label">Πρόσβαση τοποθεσίας στο παρασκήνιο</string>
<string name="permission_background_location_description">Το CAPod χρησιμοποιεί \"Πρόσβαση τοποθεσίας στο παρασκήνιο\" για να ενεργοποιεί λειτουργίες όπως \"Εμφάνιση αναδυόμενου\" και \"Αυτόματη σύνδεση\" ενώ η εφαρμογή είναι κλειστή. Η πρόσβαση τοποθεσίας στο παρασκήνιο επιτρέπει στην εφαρμογή να λαμβάνει δεδομένα χαμηλής ενέργειας Bluetooth ενώ βρίσκεται στο παρασκήνιο. Αυτή η εφαρμογή ΔΕΝ θα χρησιμοποιεί δεδομένα 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 να εμφανίζεται πάνω από άλλες εφαρμογές για να είναι δυνατή η λειτουργία \"Εμφάνιση αναδυόμενου\".</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">Αριστερό pod</string>
<string name="pods_dual_right_label">Δεξί pod</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="pods_case_unknown_state">Άγνωστη κατάσταση</string>
<string name="last_seen_x">Τελευταία εμφάνιση: %s</string>
<string name="first_seen_x">Πρώτη εμφάνιση: %s</string>
</resources>
@@ -1,10 +0,0 @@
<?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">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>
</resources>
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_grant_permission_action">Conceder permiso</string>
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicacion requiere el permiso \"Conexión Bluetooth\" para interactuar con los dispositivos vinculados e iniciar las conexiones.</string>
<string name="permission_bluetooth_scan_label">Escaneo Bluetooth</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos como tus AirPods.</string>
<string name="permission_bluetooth_description">Esta aplicacion requiere el permiso \"Bluetooth\" para conectarse a los dispositivos vinculados.</string>
<string name="permission_access_fine_location_label">Acceso a la ubicación precisa</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso de \"ubicación precisa\" para recibir Datos de Bluetooth de bajo consumo. Sus auriculares utilizan la tecnología Datos de Bluetooth de bajo consumo para transmitir su estado. Esta aplicación NO utilizará datos de Bluetooth para determinar su 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 usa el \"acceso a la ubicación en segundo plano\" para habilitar funciones como \"Mostrar ventana emergente\" 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 bajo consumo 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">Deshabilitar optimizaciones de batería</string>
<string name="permission_ignore_battery_optimizations_description">Las optimizaciones de la batería evitan que esta aplicación reciba datos de Bluetooth de manera confiable mientras la aplicación está en segundo plano.</string>
<string name="pods_dual_left_label">Audífono izquierdo</string>
<string name="pods_dual_right_label">Audífono derecho</string>
<string name="pods_case_label">Estuche</string>
<string name="pods_case_status_open_label">Estuche abierto</string>
<string name="pods_case_status_closed_label">Estuche cerrado</string>
<string name="pods_connection_state_disconnected_label">No está 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">Colgar</string>
<string name="pods_connection_state_unknown_label">Estado de conexión desconocido</string>
<string name="pods_unknown_raw_data_label">Datos sin procesar</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 ello, contáctame :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
<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="headset_being_worn_label">En uso</string>
</resources>
@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_value_not_available_label">N/D</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 suyos. Encienda y conecte su dispositivo o ajuste la configuración.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth deshabilitado</string>
<string name="overview_bluetooth_disabled_description">El Bluetooth está deshabilitado, habilítelo ;)</string>
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicación requiere el permiso \"conexión Bluetooth\" para interactuar con los dispositivos pareados, e iniciar las conexiones.</string>
<string name="permission_bluetooth_scan_label">Buscando Bluetooth</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos, como sus AirPods.</string>
<string name="permission_bluetooth_description">Esta aplicacion requiere \"Bluetooth\" para conectarse a los dispositivos pareados.</string>
<string name="permission_access_fine_location_label">Acceso a la ubicación precisa</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso de \"ubicación precisa\" para recibir datos de Bluetooth Low Energy. Sus auriculares utilizan la tecnología Bluetooth Low Energy para transmitir su estado. Esta aplicación NO utilizará datos de Bluetooth para determinar su 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 usa el \"Acceso a la ubicación en segundo plano\" para habilitar funciones como \"Mostrar ventana emergente\" 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 Low Energy 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 Optimización de Batería</string>
<string name="permission_ignore_battery_optimizations_description">La Optimización de Batería evitan que esta aplicación reciba datos de Bluetooth de manera confiable 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">Permita que CAPod dibuje sobre otras aplicaciones para que la función \"Mostrar ventana emergente\" sea posible.</string>
<string name="settings_scanner_mode_lowpower_label">Bajo consumo</string>
<string name="settings_scanner_mode_balanced_label">Balanceado</string>
<string name="settings_scanner_mode_lowlatency_label">Baja latencia</string>
<string name="settings_monitor_mode_manual_label">Cuando la aplicación 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">Cuando se ve</string>
<string name="settings_reaction_autoconnect_caseopen_label">El estuche está abierto</string>
<string name="settings_reaction_autoconnect_inear_label">En el oído</string>
<string name="pods_dual_left_label">Audífono izquierdo</string>
<string name="pods_dual_right_label">Audífono 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">Colgar</string>
<string name="pods_connection_state_unknown_label">Estado de la conexion desconocido</string>
<string name="pods_unknown_raw_data_label">Datos sin procesar</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 ello, contácteme :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
<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="headset_being_worn_label">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>
</resources>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="pods_dual_left_label">Vasak klapp</string>
<string name="pods_dual_right_label">Parem klapp</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<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="pods_dual_left_label">Kaliwang pod</string>
<string name="pods_dual_right_label">Kanan na pod</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>
</resources>
@@ -1,61 +0,0 @@
<?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 (libre)</string>
<string name="general_value_not_available_label">Non disponible</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>
<string name="overview_nomaindevice_description">Tous les appareils détectés ne sont probablement pas les vôtres. Allumez et connectez votre appareil ou réglez les paramètres.</string>
<string name="overview_bluetooth_disabled_label">Le Bluetooth est désactivé</string>
<string name="overview_bluetooth_disabled_description">Le Bluetooth est désactivé, activez-le ;)</string>
<string name="permission_bluetooth_connect_label">Connexion Bluetooth</string>
<string name="permission_bluetooth_connect_description">Cette appli exige lautorisation « Connexion Bluetooth » pour interagir avec les appareils jumelés et démarrer les connexions.</string>
<string name="permission_bluetooth_scan_label">Analyse Bluetooth</string>
<string name="permission_bluetooth_scan_description">Lautorisation « Analyse Bluetooth » permet à lappli de découvrir et de réceptionner les données Bluetooth des appareils à proximité tels que vos AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Cette appli exige lautorisation « Bluetooth » pour se connecter aux appareils Bluetooth jumelés.</string>
<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 notification » 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_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 notification » 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>
<string name="settings_monitor_mode_manual_label">Si lappli est en fonction</string>
<string name="settings_monitor_mode_automatic_label">Si lappareil est connecté</string>
<string name="settings_monitor_mode_always_label">Toujours</string>
<string name="settings_reaction_autoconnect_whenseen_label">Sil est présent</string>
<string name="settings_reaction_autoconnect_caseopen_label">Si l’étui est ouvert</string>
<string name="settings_reaction_autoconnect_inear_label">Sil est porté</string>
<string name="pods_dual_left_label">AirPod gauche</string>
<string name="pods_dual_right_label">AirPod droit</string>
<string name="pods_case_label">Étui</string>
<string name="pods_case_status_open_label">Ouvert</string>
<string name="pods_case_status_closed_label">Fermé</string>
<string name="pods_connection_state_disconnected_label">Non connecté à un appareil</string>
<string name="pods_connection_state_idle_label">Connecté à un appareil, mais au repos</string>
<string name="pods_connection_state_music_label">En mode musique</string>
<string name="pods_connection_state_call_label">En mode appel</string>
<string name="pods_connection_state_ringing_label">Sonnerie</string>
<string name="pods_connection_state_hanging_up_label">Fin de lappel</string>
<string name="pods_connection_state_unknown_label">État de connexion inconnu</string>
<string name="pods_unknown_raw_data_label">Données brutes</string>
<string name="pods_unknown_label">Appareil inconnu</string>
<string name="pods_unknown_contact_dev">Cet appareil est inconnu, mais il utilise un message de format semblable. Prenons-le en charge, contactez-moi :)</string>
<string name="pods_none_label_short">Aucun appareil</string>
<string name="pods_charging_label">En charge</string>
<string name="pods_inear_label">Dans loreille</string>
<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="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>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-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">कैपोड फोस</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">ऐप बंद होने पर \"शो पॉपअप\" और \"ऑटोकनेक्ट\" जैसी सुविधाओं को सक्षम करने के लिए कैपोड \"पृष्ठभूमि स्थान पहुंच\" का उपयोग करता है। बैकग्राउंड लोकेशन एक्सेस ऐप को ब्लूटूथ लो एनर्जी डेटा w . प्राप्त करने की अनुमति देता है</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>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_value_not_available_label">Nincs_adat</string>
<string name="general_error_label">Hiba</string>
<string name="general_grant_permission_action">Hozzáférés megadva</string>
<string name="overview_nomaindevice_label">Nincs elsődleges eszköz</string>
<string name="overview_nomaindevice_description">Valószínűleg nem minden észlelt eszköz az Öné. Kapcsolja be és csatlakoztassa a készüléket, vagy módosítsa a beállításokat.</string>
<string name="overview_bluetooth_disabled_label">A Bluetooth kikapcsolva</string>
<string name="overview_bluetooth_disabled_description">Bluetooth kikapcsolva, engedélyezd ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth_kapcsolat</string>
<string name="permission_bluetooth_connect_description">Ehhez az alkalmazáshoz a \"Bluetooth kapcsolat\" engedély szükséges a párosított eszközökkel való interakcióhoz és a kapcsolatok kezdeményezéséhez.</string>
<string name="permission_bluetooth_scan_label">Bluetooth_keresés</string>
<string name="permission_bluetooth_scan_description">A „Bluetooth-keresés” engedély lehetővé teszi az alkalmazás számára, hogy Bluetooth-adatokat keressen és fogadjon a közeli eszközökről, például az Ön AirPodjairól.</string>
<string name="permission_bluetooth_description">Ehhez az alkalmazáshoz „Bluetooth” engedély szükséges a párosított Bluetooth-eszközökhöz való csatlakozáshoz.</string>
<string name="permission_access_fine_location_label">Lokalizálás engedélyezése</string>
<string name="permission_access_fine_location_description">A CAPod a „megfelelő hely” engedélyt használja a Bluetooth alacsony töltés adatok fogadására. Fejhallgatója Bluetooth alacsony töltés technológiát használ az állapotának közvetítésére. Ez az alkalmazás NEM használ Bluetooth-adatokat az Ön tartózkodási helyének meghatározásához.</string>
<string name="permission_background_location_label">Háttér helyhozzáférés</string>
<string name="permission_background_location_description">\"Automatikus_csatlakozás\".</string>
<string name="permission_ignore_battery_optimizations_label">Az akkumulátor optimalizálás letiltása</string>
<string name="permission_ignore_battery_optimizations_description">Az akkumulátor optimalizálás megakadályozza, hogy ez az alkalmazás megbízhatóan fogadja a Bluetooth-adatokat, miközben az alkalmazás a háttérben működik.</string>
<string name="permission_required_title">A következő engedély szükséges:</string>
<string name="permission_system_alert_window_label">Rendszer figyelmeztető ablak</string>
<string name="permission_system_alert_window_description">Engedélyezze a CAPod számára, hogy más alkalmazások fölé kerüljön, hogy lehetővé tegye a „Előugró ablak” funkciót.</string>
<string name="settings_scanner_mode_lowpower_label">Kis fogyasztású</string>
<string name="settings_scanner_mode_balanced_label">Kiegyensúlyozott</string>
<string name="settings_scanner_mode_lowlatency_label">Alacsony késleltetés</string>
<string name="settings_monitor_mode_manual_label">Amikor az alkalmazás nyitva van</string>
<string name="settings_monitor_mode_automatic_label">Amikor az eszköz csatlakoztatva van</string>
<string name="settings_monitor_mode_always_label">Mindig</string>
<string name="settings_reaction_autoconnect_whenseen_label">Amikor látható</string>
<string name="settings_reaction_autoconnect_caseopen_label">Tok Nyitva</string>
<string name="settings_reaction_autoconnect_inear_label">Fülhallgató</string>
<string name="pods_dual_left_label">Bal hangszóró</string>
<string name="pods_dual_right_label">Jobb hangszóró</string>
<string name="pods_case_label">Tok</string>
<string name="pods_case_status_open_label">Nyitott</string>
<string name="pods_case_status_closed_label">Zárt</string>
<string name="pods_connection_state_disconnected_label">Nincs eszközhöz csatlakoztatva</string>
<string name="pods_connection_state_idle_label">Eszközhöz csatlakozik, de tétlen</string>
<string name="pods_connection_state_music_label">Zenei mód</string>
<string name="pods_connection_state_call_label">Hívás üzemmód</string>
<string name="pods_connection_state_ringing_label">Hívás</string>
<string name="pods_connection_state_hanging_up_label">Hívás vége</string>
<string name="pods_connection_state_unknown_label">Ismeretlen kapcsolati állapot</string>
<string name="pods_unknown_raw_data_label">Rossz adat</string>
<string name="pods_unknown_label">Ismeretlen eszköz</string>
<string name="pods_unknown_contact_dev">Ez egy ismeretlen eszköz, de hasonló üzenetformátumot használ. Adjunk hozzá támogatást, vedd fel velem a kapcsolatot :)</string>
<string name="pods_none_label_short">Nincs eszköz</string>
<string name="pods_charging_label">Töltés</string>
<string name="pods_inear_label">Fülhalgató</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">A tied</string>
<string name="headset_being_worn_label">Használt</string>
<string name="pods_case_unknown_state">Ismeretlen állapot</string>
<string name="last_seen_x">Utoljára volt látható %s</string>
<string name="first_seen_x">Először volt látható: %s</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,61 +0,0 @@
<?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">T/A</string>
<string name="general_error_label">Error</string>
<string name="general_grant_permission_action">Berikan izin</string>
<string name="overview_nomaindevice_label">Tidak ada perangkat utama</string>
<string name="overview_nomaindevice_description">Semua perangkat yang terdeteksi tidak mungkin menjadi milik anda. Nyalakan &amp; sambungkan perangkat anda atau sesuaikan pengaturan.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth dinonaktifkan</string>
<string name="overview_bluetooth_disabled_description">Bluetooth dinonaktifkan, aktifkan terlebih dahulu ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth tersambung</string>
<string name="permission_bluetooth_connect_description">Aplikasi ini memerlukan izin \"Koneksi Bluetooth\" untuk berinteraksi dengan perangkat yang akan dipasangkan &amp; memulai koneksi.</string>
<string name="permission_bluetooth_scan_label">Pemindaian Bluetooth</string>
<string name="permission_bluetooth_scan_description">Izin \"Pemindaian Bluetooth\" memungkinkan aplikasi ini menemukan &amp; menerima data Bluetooth dari perangkat terdekat seperti AirPods anda.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Aplikasi ini memerlukan izin \"Bluetooth\" untuk terhubung ke perangkat bluetooth yang ingin dipasangkan.</string>
<string name="permission_access_fine_location_label">Akses lokasi halus</string>
<string name="permission_access_fine_location_description">CAPod menggunakan izin \"lokasi halus\" untuk menerima data Bluetooth Hemat Energi. Headphone anda menggunakan teknologi Bluetooth Hemat Energi untuk menyiarkan statusnya. Aplikasi ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi anda.</string>
<string name="permission_background_location_label">Akses lokasi latar belakang</string>
<string name="permission_background_location_description">CAPod menggunakan \"akses lokasi latar belakang\" untuk mengaktifkan fitur seperti \"Tampilkan popup\" dan \"Sambungan Otomatis\" saat aplikasi ditutup. Akses lokasi latar belakang memungkinkan aplikasi menerima data Bluetooth Hemat Energi saat berada di latar belakang. Aplikasi ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi Anda.</string>
<string name="permission_ignore_battery_optimizations_label">Nonaktifkan pengoptimalan baterai</string>
<string name="permission_ignore_battery_optimizations_description">Pengoptimalan baterai mencegah aplikasi ini menerima data Bluetooth secara andal saat aplikasi berada di latar belakang.</string>
<string name="permission_required_title">Izin berikut diperlukan:</string>
<string name="permission_system_alert_window_label">Jendela Peringatan Sistem</string>
<string name="permission_system_alert_window_description">Izinkan CAPod untuk tampil di atas aplikasi lain untuk memungkinkan fitur \"Tampilkan PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Daya rendah</string>
<string name="settings_scanner_mode_balanced_label">Seimbang</string>
<string name="settings_scanner_mode_lowlatency_label">Latemsi rendah</string>
<string name="settings_monitor_mode_manual_label">Saat aplikasi terbuka</string>
<string name="settings_monitor_mode_automatic_label">Saat perangkat terhubung</string>
<string name="settings_monitor_mode_always_label">Selalu</string>
<string name="settings_reaction_autoconnect_whenseen_label">Saat terlihat</string>
<string name="settings_reaction_autoconnect_caseopen_label">Kasing terbuka</string>
<string name="settings_reaction_autoconnect_inear_label">Di telinga</string>
<string name="pods_dual_left_label">Pod kiri</string>
<string name="pods_dual_right_label">Pod kanan</string>
<string name="pods_case_label">Kasing</string>
<string name="pods_case_status_open_label">Terbuka</string>
<string name="pods_case_status_closed_label">Tertutup</string>
<string name="pods_connection_state_disconnected_label">Tidak terhubung ke perangkat</string>
<string name="pods_connection_state_idle_label">Terhubung ke perangkat, tetapi menganggur</string>
<string name="pods_connection_state_music_label">Dalam mode musik</string>
<string name="pods_connection_state_call_label">Dalam mode panggilan</string>
<string name="pods_connection_state_ringing_label">Berdering</string>
<string name="pods_connection_state_hanging_up_label">Mengangkat</string>
<string name="pods_connection_state_unknown_label">Status koneksi tidak diketahui</string>
<string name="pods_unknown_raw_data_label">Data mentah</string>
<string name="pods_unknown_label">Perangkat tidak dikenal</string>
<string name="pods_unknown_contact_dev">Ini adalah perangkat yang tidak dikenal, tapi menggunakan format pesan yang serupa. Mari tambahkan dukungan untuk itu, hubungi saya :)</string>
<string name="pods_none_label_short">Tidak ada perangkat</string>
<string name="pods_charging_label">Mengisi</string>
<string name="pods_inear_label">Di telinga</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Milikmu</string>
<string name="headset_being_worn_label">Sedang dipakai</string>
<string name="pods_case_unknown_state">Status tidak diketahui</string>
<string name="last_seen_x">Terakhir terlihat: %s</string>
<string name="first_seen_x">Pertama kali terlihat: %s</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,61 +0,0 @@
<?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">N/A</string>
<string name="general_error_label">Errore</string>
<string name="general_grant_permission_action">Concedi permessi</string>
<string name="overview_nomaindevice_label">Nessun device primario</string>
<string name="overview_nomaindevice_description">Tutti i dispositivi rilevati molto probabilmente non sono tuoi. Accendi e connetti i tuoi dispositivi oppure sistema le impostazioni.</string>
<string name="overview_bluetooth_disabled_label">Il Bluetooth é disabilitato</string>
<string name="overview_bluetooth_disabled_description">Il bluetooth é disabilitato, abilitalo ;)</string>
<string name="permission_bluetooth_connect_label">Connesione bluetooth</string>
<string name="permission_bluetooth_connect_description">Questa applicazione richiede il permesso di \"Connesione bluetooth\" per interagire con i dispotivi collegati e iniziare le connesioni.</string>
<string name="permission_bluetooth_scan_label">Scansione bluetooth</string>
<string name="permission_bluetooth_scan_description">Il permesso di \"Scansione bluetooth\" permette a questa app di trovare e ricevere dati Bluetooth da dispositivi vicini come le tue Airpods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Questa app richiede il permesso \"Bluetooth\" per connettersi ai dispoitivi Bluetooth.</string>
<string name="permission_access_fine_location_label">Accedi alla posizione precisa</string>
<string name="permission_access_fine_location_description">CAPod usa i permessi di Geolocalizzazione per ricevere dati Bluetooth a basso consumo. Le tue cuffie suano la tecnologia Bluetooth a basso consumo per inviare al dispositivo al quale sono connesse il loro stato. Questa app non utilizza i dati Bluetooth per geolocalizzarti.</string>
<string name="permission_background_location_label">Accesso in background alla tua posizione</string>
<string name="permission_background_location_description">CAPod usa l\'accesso in background alla tua posizione per far funzionare opzioni come \"Mostra popup\" e \"AutoConnect\" mentre l\'applicazione é chiusa. L\'accesso in background abiliterà l\'applicazione nella ricezione di dati Bluetooth a basso consume mentre l\'app non é aperta. Questa applicazione non utilizza i dati Bluetooth per geolocalizzarti.</string>
<string name="permission_ignore_battery_optimizations_label">Disabilita ottimizzazione batteria</string>
<string name="permission_ignore_battery_optimizations_description">L\'ottimizzazione della batteria impedisce a questa applicazione di ricevere dati Bluetooth mentre l\'app é chiusa.</string>
<string name="permission_required_title">È richiesta la seguente autorizzazione:</string>
<string name="permission_system_alert_window_label">Finestra di avviso di sistema</string>
<string name="permission_system_alert_window_description">Permetti a CAPod di far apprarire overlay sopra le altre applicazioni per far funzionare \"Mostra PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Basso consumo</string>
<string name="settings_scanner_mode_balanced_label">Bilanciato</string>
<string name="settings_scanner_mode_lowlatency_label">Bassa latenza</string>
<string name="settings_monitor_mode_manual_label">Quando l\'applicazione è aperta</string>
<string name="settings_monitor_mode_automatic_label">Quando il device è connesso</string>
<string name="settings_monitor_mode_always_label">Sempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Quando rilevato</string>
<string name="settings_reaction_autoconnect_caseopen_label">La custodia é aperta</string>
<string name="settings_reaction_autoconnect_inear_label">Nell\'orecchio</string>
<string name="pods_dual_left_label">Pod sinistra</string>
<string name="pods_dual_right_label">Pod destra</string>
<string name="pods_case_label">Custodia</string>
<string name="pods_case_status_open_label">Aperto</string>
<string name="pods_case_status_closed_label">Chiuso</string>
<string name="pods_connection_state_disconnected_label">Non connesso</string>
<string name="pods_connection_state_idle_label">Connesso ad un device, ma inattivo</string>
<string name="pods_connection_state_music_label">In modalità musica</string>
<string name="pods_connection_state_call_label">In modalità chiamata</string>
<string name="pods_connection_state_ringing_label">Squilla</string>
<string name="pods_connection_state_hanging_up_label">Riconnessione</string>
<string name="pods_connection_state_unknown_label">Stato di connessione sconosciuto</string>
<string name="pods_unknown_raw_data_label">Dati non elaborati</string>
<string name="pods_unknown_label">Dispositivo sconosciuto</string>
<string name="pods_unknown_contact_dev">Questo é un dispositivo sconosciutom ma sta utilizzando un formato di comunicazione noto. Aggiungiamo il supporto per questo device, Contattami! :)</string>
<string name="pods_none_label_short">Nessun dispositivo</string>
<string name="pods_charging_label">In ricarica</string>
<string name="pods_inear_label">Nell\'orecchio</string>
<string name="pods_microphone_label">Microfono</string>
<string name="pods_yours">Tuoi</string>
<string name="headset_being_worn_label">Indossato</string>
<string name="pods_case_unknown_state">Stato sconosciuto</string>
<string name="last_seen_x">Ultimo accesso: %s</string>
<string name="first_seen_x">Primo accesso: %s</string>
</resources>
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<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">בלוטות\' מכובה, יש להדליקו ;)</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">בלוטות</string>
<string name="permission_bluetooth_description">אפליקציה זו דורשת הרשאת \"בלוטות\'\" כדי להתחבר למכשירי בלוטות\' מותאמים.</string>
<string name="permission_access_fine_location_label">גישה למיקום מדוייק</string>
<string name="permission_access_fine_location_description">CAPod משתמש בהרשאת \"שיפור מיקום\" כדי לקבל הפחתת צריכת אנרגיה בנתוני Bluetooth. האוזניות שלך משתמשות בטכנולוגיית 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 לצייר מעל אפליקציות אחרות כדי לאפשר את התכונה \"הצג חלון הודעות\".</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="pods_case_unknown_state">מצב לא ידוע</string>
<string name="last_seen_x">נראה לאחרונה: %s</string>
<string name="first_seen_x">נראה לראשונה: %s</string>
</resources>
@@ -1,61 +0,0 @@
<?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">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スキャン」権限により、このアプリはAirPodsなどの近くのデバイスからBluetoothのデータを検出して受信することができます。</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が他のアプリの上に描画できるようにして、「ポップアップ表示」機能を有効にします。</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">RAWデータ</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="pods_case_unknown_state">不明な状態</string>
<string name="last_seen_x">最後に確認した時間:%s</string>
<string name="first_seen_x">最初に確認した時間:%s</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,61 +0,0 @@
<?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">N/A</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는 저전력 블루투스 정보를 받기 위해 정밀 위치 접근 권한을 사용합니다. 앱은 블루투스 헤드폰의 저전력 블루투스 기술을 사용해 기기의 상태에 접근합니다. CAPod는 이 권한으로 사용자의 위치를 확인하지 않습니다.</string>
<string name="permission_background_location_label">백그라운드 위치 접근</string>
<string name="permission_background_location_description">CAPod는 팝업 보기나 자동 연결과 같은 기능을 앱이 꺼져있는 상태에서도 사용할 수 있게 하기 위해 백그라운드 위치 접근 권한을 사용합니다. 백그라운드 위치 접근 권한은 백그라운드에서도 저전력 블루투스 정보를 받을 수 있게 합니다. 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="pods_case_unknown_state">알 수 없는 상태</string>
<string name="last_seen_x">마지막으로 확인: %s</string>
<string name="first_seen_x">처음 확인: %s</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>

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