Compare commits

...
450 Commits
Author SHA1 Message Date
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
darken 07be4b8258 Release: 2.3.1-rc0 2022-11-07 22:51:12 +01:00
darken 217087c6d4 Clean up tests 2022-11-07 22:50:52 +01:00
Matthias Urhahn 2315a18c86 Improve history caching for AirPods. (#55)
Don't throw away the last history item that contains case infos.
2022-11-07 22:28:13 +01:00
Matthias Urhahn f59e9d2dc5 Fix comparison that determines which list item is the primary device. (#54) 2022-11-07 19:42:31 +01:00
Matthias Urhahn 1da220085c Merge pull request #53 from d4rken-org/improve_automatic_monitor_mode
Improve automatic monitor mode
2022-11-07 19:36:30 +01:00
Matthias Urhahn 962b2a01f1 Merge branch 'main' into improve_automatic_monitor_mode 2022-11-07 19:35:49 +01:00
darken eeda14f424 When in MonitorMode.AUTOMATIC and there is no mainDeviceAddress set, then just keep running while there is any connected headset device. 2022-11-07 19:25:31 +01:00
darken 6269e20085 When selecting automatic monitor-mode, check if the user has selected a device address, if not pop up the selection dialog. 2022-11-07 19:08:52 +01:00
Matthias Urhahn 3d05296eb2 Merge pull request #51 from GitGitro/main
add monochrome icon
2022-11-07 18:36:48 +01:00
GitGitro e90ab95cd0 add monochrome icon, closes #50,closes #23 2022-11-07 15:31:31 +01:00
Matthias Urhahn 23ec2f025e Update README.md 2022-11-05 12:31:21 +01:00
darken 624898dd5e Release: 2.3.0-rc0 2022-11-05 12:20:06 +01:00
darken c6d6604399 Merge remote-tracking branch 'origin/main' into main 2022-11-05 12:19:19 +01:00
darken 7473704954 Update changelog 2022-11-05 12:19:14 +01:00
Matthias Urhahn 7a6b216bf6 Update translations (#49) 2022-11-05 12:11:54 +01:00
Matthias Urhahn 1ef09124af Improve notification theming (#47)
* Make notification themes more dynamic to adapt to day/night/system themes.

* Fix broken style references
2022-11-05 12:11:33 +01:00
darken 6340f73d61 Throttle pod device data for the UI, notifications or widgets.
We don't need faster updates than 1 per second.
(We keep reactions at maximum speed tho)
2022-11-05 12:11:09 +01:00
darken b9f4992a33 A CAPod widget, oh my! ¯\_(ツ)_/¯ 2022-11-05 11:53:09 +01:00
darken 07a1fee013 Fix proguard rule inclusion 2022-10-28 22:27:50 +02:00
darken 63f37e51ad Release: 2.2.1-rc1 2022-10-27 17:40:57 +02:00
darken 01ea08a5cb Update release script 2022-10-27 17:38:50 +02:00
darken bddb20c5bb More version plumping 2022-10-27 17:38:50 +02:00
darken f9a97297da Add a release script base.
Based on https://gist.github.com/jv-k/703e79306554c26a65a7cfdb9ca119c6

WIP
2022-10-27 17:38:50 +02:00
darken 5fc2b56635 Update translations 2022-10-27 08:09:49 +02:00
darken 172953ccb3 Fix monitor mode being set to ALWAYS when visiting the reactions settings screen.
Fixes #41
2022-10-27 07:59:21 +02:00
darken d55300f53d Fix bold highlighting of the main pod device not being reset. 2022-10-22 10:49:45 +03:00
darken c3d4500e3a Version bump (v2.2.1-rc0) 2022-09-24 15:50:25 +02:00
darken cf72e28727 Fix app crash if Bluetooth adapter is null and we can't retrieve the list of bonded devices in settings. 2022-09-24 15:49:56 +02:00
darken b488a1be16 Version bump (v2.2.0-rc0) 2022-09-24 12:00:21 +02:00
darken 805d0cb4b6 Update translations 2022-09-24 11:59:58 +02:00
darken fa045c2504 Increase worker scheduling flex interval for WearOS. 2022-09-24 11:59:09 +02:00
Matthias Urhahn 23fd373a9b AirPods Pro 2 (#32)
* Preliminary support for AirPods Pro 2, based on #31, assuming identifier is `11 20`

* Fix AirPods Pro 2 identifier (it's 0x1420)

* Add second AirPods Pro 2 test case from reddit user.

* Update Readme with AirPods Pro 2
2022-09-24 11:51:53 +02:00
darken ab619cb073 Version bump (2.1.1-rc0) 2022-09-19 22:26:43 +02:00
darken 2d96822014 Add request for POST_NOTIFICATIONS on Android 13.
Closes #29
2022-09-19 22:26:43 +02:00
darken 887adc87de Reduce background monitor time to reduce battery usage. 2022-09-16 09:31:18 +02:00
darken 4219bd02c5 Setup fastlane for uploading app and wearos AAB 2022-09-14 21:40:13 +02:00
darken 48544464ae Version bump 2022-09-14 21:19:07 +02:00
darken 9b4f2477e4 Add sponsor link to settings toolbar 2022-09-14 21:15:21 +02:00
darken 26e8840484 On Foss upgrade navigate to gituhb sponsors directly 2022-09-14 21:06:09 +02:00
darken b6ef6d3ce9 Fix upgrade check 2022-09-14 21:00:15 +02:00
darken cdfcd24049 Ensure that BLEScanner only starts when all necessary permissions are granted.
(Or rather that scanner is restarted when permissions change).
+ some refactoring to improve readability
2022-09-14 20:51:39 +02:00
darken 95a8e0b125 Version bump 2022-09-14 19:55:56 +02:00
darken 84b7509f33 Adjust versioning for GPlay wear os bundle upload 2022-09-14 19:39:30 +02:00
darken a53baec9dc Version bump 2022-09-14 18:59:08 +02:00
darken 4b4bc86cf7 Fix Github release for WearOS APK 2022-09-14 18:31:34 +02:00
darken 4d3860089e Add fastlane wear screenshots 2022-09-14 18:00:18 +02:00
darken 758f55f13c Update README.md 2022-09-14 18:00:18 +02:00
darken 1e191c3adc More translation updates 2022-09-14 18:00:18 +02:00
darken 1ba2cf7c9d Sync translations with Crowdin (remove duplicate strings between modules) 2022-09-14 18:00:18 +02:00
darken 9ed1cc00d5 Fix crowdin configuration 2022-09-14 18:00:18 +02:00
darken 3be1173197 Tweak dual pods wear layout 2022-09-14 18:00:18 +02:00
darken f6e69c62b1 Clean up strings 2022-09-14 18:00:18 +02:00
darken 6d9679a7fc More cleanup 2022-09-14 18:00:18 +02:00
darken 6ea260a81f More resource cleanup 2022-09-14 18:00:18 +02:00
darken 26da600761 Fix module packagename 2022-09-14 18:00:18 +02:00
darken 33172a7aee Use the default low_latency mode for wearOS too, we want to get as many BLEScanRecords as possible in a short time frame and then sleep again. 2022-09-14 18:00:18 +02:00
darken 7eb9f6ac95 Fix failing test 2022-09-14 18:00:18 +02:00
darken fc06c285d5 Fix periodic checks aborting with no data. 2022-09-14 18:00:18 +02:00
darken ca2e4fec79 Implement periodic checks for wear 2022-09-14 18:00:18 +02:00
darken 87ca90f6cc Fix "last seen at". 2022-09-14 18:00:18 +02:00
darken 7dfea78c87 Simplify WearOS app because this is such a cluster fuck and I've ran out of motivation. 2022-09-14 18:00:18 +02:00
darken d7808009dc Tweak main overview 2022-09-14 18:00:18 +02:00
darken 62afb7a130 Cache main pod device 2022-09-14 18:00:18 +02:00
darken 87bef9cb9c Icon and reintroduce splash screen 2022-09-14 18:00:18 +02:00
darken 3860cb21c6 Code fluff 2022-09-14 18:00:18 +02:00
darken bf4ad7886c Improve wear os overview screen 2022-09-14 18:00:18 +02:00
darken 1d51c207f4 Basic working data display in wear-app 2022-09-14 18:00:18 +02:00
darken 2eb3f70e0c Wear UI wip 2022-09-14 18:00:18 +02:00
darken eb4d883194 More refactoring and modularization 2022-09-14 18:00:18 +02:00
darken 3f5c9ff9de Fix reaction settings fragment crash 2022-09-14 18:00:18 +02:00
darken 47039796de Nicer text display if we don't use raw github 2022-09-14 18:00:18 +02:00
darken fd054abcca Add codestyle 2022-09-14 18:00:18 +02:00
darken 2861bfbd47 Fix worker inject 2022-09-14 18:00:18 +02:00
darken 6ea3127f9c More refactoring to split up modules, both compile.
Wear is still WIP.
2022-09-14 18:00:18 +02:00
darken f7acb5e115 Fix copy paste regressions 2022-09-14 18:00:18 +02:00
darken b1ad1f7699 Cleanup 2022-09-14 18:00:18 +02:00
darken ab713df3c0 Fix warnings 2022-09-14 18:00:18 +02:00
darken e0d44f2f1c Use Kotlin for Gradle DSL 2022-09-14 18:00:18 +02:00
darken 61463bc05f More refactoring 2022-09-14 18:00:18 +02:00
darken a219fc3527 Fix build issues 2022-09-14 18:00:18 +02:00
darken 1cd4028ce8 Adjust crowdin config 2022-09-14 18:00:18 +02:00
darken de9a30eb93 Refactoring 2022-09-14 18:00:18 +02:00
darken 2210561af3 Refactoring 2022-09-14 18:00:18 +02:00
darken c5ddc474c4 Update translations 2022-09-14 18:00:18 +02:00
darken 688748da36 Go multi-module: app-common, app-phone, app-wear 2022-09-14 18:00:18 +02:00
darken 753d311593 Tweak versioning and bump (1.3.14-rc0) 2022-08-24 11:56:42 +02:00
darken 5bae3d9931 Remove in-app email shortcut 2022-08-24 11:40:16 +02:00
darken a3495a8218 Update translations 2022-08-24 11:33:48 +02:00
Poussinou 5fa68f77a2 Update README.md 2022-07-31 11:13:51 +02:00
darken dbba2e9791 Another fastlane gplay upload fix. 2022-07-25 21:00:43 +02:00
darken 6207f26a68 Update fastlane to 2.208 2022-07-25 20:57:24 +02:00
darken 7fcd39822e Update fastlane to 2.208 2022-07-25 20:56:45 +02:00
darken d20f8a604f Fix CI issues with Google Play upload (part 2...) 2022-07-25 20:47:26 +02:00
886 changed files with 23854 additions and 7586 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"permissions": {
"allow": [
"mcp__ide__getDiagnostics",
"Bash(./gradlew tasks:*)",
"Bash(./gradlew:*)",
"Bash(find:*)",
"Bash(ls:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"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": []
}
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Script to fix import statements after removing app-common module.
This script fixes R class imports from app-common to app module.
"""
import os
import re
from pathlib import Path
def fix_r_import(file_path):
"""Fix R import in a single file."""
print(f"Fixing R import in: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace the import statement
updated_content = re.sub(
r'import eu\.darken\.capod\.common\.R$',
'import eu.darken.capod.R',
content,
flags=re.MULTILINE
)
if updated_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Updated import in {file_path}")
return True
else:
print(f" - No changes needed in {file_path}")
return False
def main():
"""Main function to fix all import statements."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that import eu.darken.capod.common.R
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
with open(kt_file, 'r', encoding='utf-8') as f:
content = f.read()
if re.search(r'import eu\.darken\.capod\.common\.R$', content, re.MULTILINE):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files with incorrect R imports")
success_count = 0
for file_path in files_to_fix:
if fix_r_import(file_path):
success_count += 1
print(f"\nFixed imports in {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
Script to fix fully qualified R references in code.
This script finds and replaces eu.darken.capod.common.R with R.
"""
import os
import re
from pathlib import Path
def fix_qualified_r_references(file_path):
"""Fix fully qualified R references in a single file."""
print(f"Fixing qualified R references in: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace fully qualified references
updated_content = re.sub(
r'\beu\.darken\.capod\.common\.R\.',
'R.',
content
)
if updated_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Fixed qualified R references in {file_path}")
return True
else:
print(f" - No changes needed in {file_path}")
return False
def main():
"""Main function to fix all qualified R references."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that have qualified R references
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
with open(kt_file, 'r', encoding='utf-8') as f:
content = f.read()
if re.search(r'\beu\.darken\.capod\.common\.R\.', content):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files with qualified R references")
success_count = 0
for file_path in files_to_fix:
if fix_qualified_r_references(file_path):
success_count += 1
print(f"\nFixed qualified R references in {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
Script to fix remaining R import issues.
This script finds files that use R.* but don't have R imports and adds them.
"""
import os
import re
from pathlib import Path
def needs_r_import(file_path):
"""Check if file uses R.* but doesn't import R."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check if file uses R.something
uses_r = re.search(r'\bR\.[a-zA-Z_]', content)
if not uses_r:
return False
# Check if file already imports R
has_import = re.search(r'import.*\.R$', content, re.MULTILINE)
if has_import:
return False
return True
def add_r_import(file_path):
"""Add R import to a file."""
print(f"Adding R import to: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find the package line
package_match = re.search(r'^package\s+[^\n]+$', content, re.MULTILINE)
if not package_match:
print(f" Error: No package declaration found in {file_path}")
return False
# Find existing imports
import_pattern = r'^import\s+[^\n]+$'
existing_imports = list(re.finditer(import_pattern, content, re.MULTILINE))
if existing_imports:
# Insert after the last import
last_import = existing_imports[-1]
insert_pos = last_import.end()
updated_content = (content[:insert_pos] +
"\nimport eu.darken.capod.R" +
content[insert_pos:])
else:
# Insert after package line
insert_pos = package_match.end()
updated_content = (content[:insert_pos] +
"\n\nimport eu.darken.capod.R" +
content[insert_pos:])
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Added R import to {file_path}")
return True
def main():
"""Main function to fix all R import issues."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that need R import
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
if needs_r_import(kt_file):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files that need R imports")
success_count = 0
for file_path in files_to_fix:
if add_r_import(file_path):
success_count += 1
print(f"\nAdded R imports to {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Script to merge strings.xml files from app-common module to app module.
This script merges all string entries from app-common into the corresponding app module files.
"""
import os
import re
import xml.etree.ElementTree as ET
from pathlib import Path
def merge_strings_xml(app_common_file, app_file):
"""Merge strings from app-common file into app file."""
print(f"Processing: {app_common_file.name} -> {app_file.name}")
# Read the app-common strings.xml content
with open(app_common_file, 'r', encoding='utf-8') as f:
common_content = f.read()
# Read the app strings.xml content
with open(app_file, 'r', encoding='utf-8') as f:
app_content = f.read()
# Extract string entries from app-common (including comments)
# Find everything between <resources> and </resources> tags, excluding the tags themselves
common_match = re.search(r'<resources[^>]*>(.*?)</resources>', common_content, re.DOTALL)
if not common_match:
print(f" Warning: No resources found in {app_common_file}")
return False
common_strings = common_match.group(1).strip()
if not common_strings:
print(f" Warning: No string content found in {app_common_file}")
return False
# Find the insertion point in the app file (before </resources>)
app_match = re.search(r'(.*?)(\s*</resources>)', app_content, re.DOTALL)
if not app_match:
print(f" Error: Invalid XML structure in {app_file}")
return False
# Merge the content
before_closing = app_match.group(1)
closing_tag = app_match.group(2)
# Add the common strings with proper spacing
merged_content = f"{before_closing}\n\n <!-- Strings from app-common -->\n{common_strings}\n{closing_tag}"
# Write the merged content back to the app file
with open(app_file, 'w', encoding='utf-8') as f:
f.write(merged_content)
print(f" ✓ Merged successfully")
return True
def main():
"""Main function to merge all strings.xml files."""
project_root = Path.cwd()
app_common_res = project_root / "app-common" / "src" / "main" / "res"
app_res = project_root / "app" / "src" / "main" / "res"
if not app_common_res.exists():
print(f"Error: app-common resources directory not found: {app_common_res}")
return 1
if not app_res.exists():
print(f"Error: app resources directory not found: {app_res}")
return 1
# Find all strings.xml files in app-common
common_strings_files = list(app_common_res.glob("*/strings.xml"))
if not common_strings_files:
print("Error: No strings.xml files found in app-common")
return 1
print(f"Found {len(common_strings_files)} strings.xml files to merge")
success_count = 0
error_count = 0
for common_file in sorted(common_strings_files):
# Determine the corresponding app file
locale_dir = common_file.parent.name
app_file = app_res / locale_dir / "strings.xml"
if not app_file.exists():
print(f" Error: Corresponding app file does not exist: {app_file}")
error_count += 1
continue
if merge_strings_xml(common_file, app_file):
success_count += 1
else:
error_count += 1
print(f"\nMerge complete:")
print(f" ✓ Successfully merged: {success_count}")
print(f" ✗ Errors: {error_count}")
return 0 if error_count == 0 else 1
if __name__ == "__main__":
exit(main())
+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,Beta,Release ]
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
+21 -29
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,11 +56,10 @@ 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 }}
@@ -74,7 +71,7 @@ jobs:
- name: Create release
if: "!contains(steps.tagger.outputs.tag, '-beta')"
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda #v2.2.1
with:
prerelease: false
tag_name: ${{ steps.tagger.outputs.tag }}
@@ -86,7 +83,7 @@ jobs:
release-gplay:
name: Create Google Play release
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
environment: gplay-production
steps:
- name: Decode Keystore
@@ -109,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
@@ -141,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')"
@@ -149,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 }}
+6 -5
View File
@@ -1,9 +1,6 @@
.idea
.gradle
build/
/fastlane/report.xml
*.iml
local.properties
.gradle
/local.properties
.DS_Store
/build
/captures
@@ -14,3 +11,7 @@ local.properties
!/.idea/codeStyles/**/*
*.jks
.local/*
/fastlane/report.xml
/fastlane/Appfile
/fastlane/README.md
.kotlin
+168
View File
@@ -0,0 +1,168 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<JSON>
<option name="OBJECT_WRAPPING" value="1" />
<option name="ARRAY_WRAPPING" value="1" />
</JSON>
<JavaCodeStyleSettings>
<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" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="FIELD_ANNOTATION_WRAP" value="1" />
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="METHOD_ANNOTATION_WRAP" value="1" />
<option name="CLASS_ANNOTATION_WRAP" value="1" />
<option name="FIELD_ANNOTATION_WRAP" value="1" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+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 %}
+158
View File
@@ -0,0 +1,158 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Build Commands
```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 Commands
```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 Commands
```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 Commands
```bash
./gradlew assembleFossRelease assembleGplayRelease
```
## Architecture Overview
### Multi-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app
### Core Architecture Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Extensive use of 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 background monitoring worker lifecycle
- `MonitorWorker`: Background worker 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**: 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 Architecture
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorWorker` 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
### 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
- **WorkManager**: Background task scheduling for monitoring
- **Moshi**: JSON serialization for configuration and debugging
- **Material Design**: UI components following Material Design guidelines
## Development Notes
### Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in
`MonitorWorker` which runs as a long-lived background task.
### 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.
### 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
Examples of correct string naming:
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)
+1
View File
@@ -0,0 +1 @@
capod.darken.eu
+103 -92
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.554.0)
aws-sdk-core (3.126.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
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)
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.4.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.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.91.0)
faraday (1.9.3)
excon (0.109.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -53,27 +58,27 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
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.204.3)
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.16.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.2)
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.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 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.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.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.1.0)
faraday (>= 0.17.3, < 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.4)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.5.0)
json (2.6.1)
jwt (2.3.0)
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.6)
rake (13.0.6)
representable (3.1.1)
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.16.0)
rubyzip (2.4.1)
security (0.1.5)
signet (0.18.0)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
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)
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.21.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.
+29 -4
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,6 +17,7 @@ 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.
* Widgets
CAPod is ad-free. Some additional features require an in-app purchase.
@@ -23,19 +26,36 @@ Currently supported models:
* AirPods 1. Generation
* AirPods 2. Generation
* AirPods 3. Generation
* AirPods Pro
* 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
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
* [GitHub](https://github.com/d4rken-org/capod/releases/latest)
| 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
@@ -51,6 +71,7 @@ Currently supported models:
## Screenshots
<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
@@ -60,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
View File
@@ -0,0 +1 @@
3.0.0-beta1 30000010
+29
View File
@@ -0,0 +1,29 @@
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
- CLAUDE.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>
-295
View File
@@ -1,295 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
id 'androidx.navigation.safeargs.kotlin'
id 'com.bugsnag.android.gradle'
id 'dagger.hilt.android.plugin'
}
def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
def buildTime = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT+1"))
android {
def packageName = "eu.darken.capod"
compileSdkVersion buildConfig.compileSdk
defaultConfig {
applicationId "${packageName}"
minSdkVersion buildConfig.minSdk
targetSdkVersion buildConfig.targetSdk
versionCode buildConfig.version.code
versionName buildConfig.version.name
testInstrumentationRunner "eu.darken.capod.HiltTestRunner"
buildConfigField "String", "GITSHA", "\"${gitSha}\""
buildConfigField "String", "BUILDTIME", "\"${buildTime}\""
manifestPlaceholders = [bugsnagApiKey: "fake"]
}
signingConfigs {
releaseFoss {}
releaseGplay {}
}
signingConfigs {
releaseFoss {
def signingFossPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing-foss.properties")
Properties signingPropsFoss = new Properties()
if (signingFossPropFile.canRead()) signingPropsFoss.load(new FileInputStream(signingFossPropFile))
String keyStorePathFoss = System.getenv("STORE_PATH") ?: signingPropsFoss["release.storePath"]
File keyStoreFoss = keyStorePathFoss ? new File(keyStorePathFoss) : null
if (keyStoreFoss?.canRead()) {
storeFile keyStoreFoss
storePassword System.getenv("STORE_PASSWORD") ?: signingPropsFoss['release.storePassword']
keyAlias System.getenv("KEY_ALIAS") ?: signingPropsFoss['release.keyAlias']
keyPassword System.getenv("KEY_PASSWORD") ?: signingPropsFoss['release.keyPassword']
}
}
releaseGplay {
def signingGplayPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing-gplay.properties")
Properties signingPropsGplay = new Properties()
if (signingGplayPropFile.canRead()) signingPropsGplay.load(new FileInputStream(signingGplayPropFile))
String keyStorePathGplay = System.getenv("STORE_PATH") ?: signingPropsGplay["release.storePath"]
File keyStoreGplay = keyStorePathGplay ? new File(keyStorePathGplay) : null
if (keyStoreGplay?.canRead()) {
storeFile keyStoreGplay
storePassword System.getenv("STORE_PASSWORD") ?: signingPropsGplay['release.storePassword']
keyAlias System.getenv("KEY_ALIAS") ?: signingPropsGplay['release.keyAlias']
keyPassword System.getenv("KEY_PASSWORD") ?: signingPropsGplay['release.keyPassword']
}
}
}
flavorDimensions "version"
productFlavors {
foss {
signingConfig signingConfigs.releaseFoss
}
gplay {
signingConfig signingConfigs.releaseGplay
}
}
Properties bugsnagProps = new Properties()
def bugsnagPropsFile = new File(System.properties['user.home'], ".appconfig/${packageName}/bugsnag.properties")
if (bugsnagPropsFile.canRead()) bugsnagProps.load(new FileInputStream(bugsnagPropsFile))
String bugSnagApiKey = System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey", "")
buildTypes {
def proguardRulesRelease = fileTree(dir: "../proguard", include: ["*.pro"]).asList().toArray()
debug {
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
proguardFiles proguardRulesRelease
proguardFiles 'proguard-rules-debug.pro'
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
}
beta {
lintOptions {
abortOnError true
fatal 'StopShip'
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
proguardFiles proguardRulesRelease
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
}
release {
lintOptions {
abortOnError true
fatal 'StopShip'
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
proguardFiles proguardRulesRelease
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
}
}
applicationVariants.all { variant ->
def flavor = variant.productFlavors[0].name.toUpperCase()
def buildType = variant.buildType.name.toUpperCase()
def versionCode = defaultConfig.versionCode
def versionName = defaultConfig.versionName
if (variant.buildType.name == "debug") {
variant.mergedFlavor.resourceConfigurations.clear()
variant.mergedFlavor.resourceConfigurations.add("en")
variant.mergedFlavor.resourceConfigurations.add("de")
} else if (variant.buildType.name != "debug") {
variant.outputs.each { output ->
output.outputFileName = "${packageName}-v${versionName}(${versionCode})-${gitSha}-${flavor}-${buildType}.apk"
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += [
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlin.time.ExperimentalTime",
"-Xuse-experimental=kotlin.ExperimentalUnsignedTypes",
"-Xuse-experimental=kotlin.contracts.ExperimentalContracts",
"-Xopt-in=kotlin.RequiresOptIn"
]
}
}
testOptions {
unitTests.all {
useJUnitPlatform()
}
unitTests {
includeAndroidResources = true
}
}
sourceSets {
test {
java.srcDirs += "$projectDir/src/testShared/java"
}
androidTest {
java.srcDirs += "$projectDir/src/testShared/java"
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin.core}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlin.coroutines}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlin.coroutines}"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin.core}"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlin.coroutines}"
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlin.coroutines}") {
// conflicts with mockito due to direct inclusion of byte buddy
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
}
// Debugging
implementation('com.bugsnag:bugsnag-android:5.9.2')
implementation 'com.getkeepsafe.relinker:relinker:1.4.3'
implementation("com.squareup.moshi:moshi:1.13.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
// DI
implementation "com.google.dagger:dagger:${versions.dagger.core}"
implementation "com.google.dagger:dagger-android:${versions.dagger.core}"
kapt "com.google.dagger:dagger-compiler:${versions.dagger.core}"
kapt "com.google.dagger:dagger-android-processor:${versions.dagger.core}"
implementation "com.google.dagger:hilt-android:${versions.dagger.core}"
kapt "com.google.dagger:hilt-android-compiler:${versions.dagger.core}"
testImplementation "com.google.dagger:hilt-android-testing:${versions.dagger.core}"
kaptTest "com.google.dagger:hilt-android-compiler:${versions.dagger.core}"
androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.dagger.core}"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.dagger.core}"
kapt "androidx.hilt:hilt-compiler:1.0.0"
implementation 'androidx.hilt:hilt-common:1.0.0'
kaptTest "androidx.hilt:hilt-compiler:1.0.0"
testImplementation 'androidx.hilt:hilt-common:1.0.0'
// Support libs
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.navigation:navigation-ui-ktx:2.3.5"
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
def work_version = "2.7.1"
implementation "androidx.work:work-runtime:${work_version}"
testImplementation "androidx.work:work-testing:${work_version}"
implementation "androidx.work:work-runtime-ktx:${work_version}"
implementation 'androidx.hilt:hilt-work:1.0.0'
// IAP
gplayImplementation 'com.android.billingclient:billing:4.0.0'
// UI
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'com.google.android.material:material:1.6.0-alpha01'
// Testing
testImplementation 'junit:junit:4.13.2'
testImplementation "org.junit.vintage:junit-vintage-engine:5.7.1"
testImplementation "androidx.test:core-ktx:1.4.0"
testImplementation "io.mockk:mockk:1.12.1"
androidTestImplementation "io.mockk:mockk-android:1.11.0"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.1"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.1"
testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.1"
androidTestImplementation "androidx.navigation:navigation-testing:2.3.5"
testImplementation "io.kotest:kotest-runner-junit5:4.6.2"
testImplementation "io.kotest:kotest-assertions-core-jvm:4.6.2"
testImplementation "io.kotest:kotest-property-jvm:4.6.2"
androidTestImplementation "io.kotest:kotest-assertions-core-jvm:4.6.2"
androidTestImplementation "io.kotest:kotest-property-jvm:4.6.2"
testImplementation 'android.arch.core:core-testing:1.1.1'
androidTestImplementation 'android.arch.core:core-testing:1.1.1'
debugImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
}
+162
View File
@@ -0,0 +1,162 @@
plugins {
id("com.android.application")
id("kotlin-android")
id("com.google.devtools.ksp")
id("kotlin-kapt")
id("kotlin-parcelize")
}
apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin")
android {
compileSdk = ProjectConfig.compileSdk
namespace = ProjectConfig.packageName
defaultConfig {
applicationId = ProjectConfig.packageName
minSdk = ProjectConfig.minSdk
targetSdk = ProjectConfig.targetSdk
versionCode = ProjectConfig.Version.code + 0 // Base app
versionName = ProjectConfig.Version.name
testInstrumentationRunner = "eu.darken.capod.HiltTestRunner"
}
// Enable automatic per-app language preferences generation
androidResources {
generateLocaleConfig = true
}
signingConfigs {
val basePath = File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}")
create("releaseFoss") {
setupCredentials(File(basePath, "signing-foss.properties"))
}
create("releaseGplay") {
setupCredentials(File(basePath, "signing-gplay-upload.properties"))
}
}
flavorDimensions.add("version")
productFlavors {
create("foss") {
dimension = "version"
signingConfig = signingConfigs["releaseFoss"]
// The info block is encrypted and can only be read by google
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
}
create("gplay") {
dimension = "version"
signingConfig = signingConfigs["releaseGplay"]
}
}
buildTypes {
val customProguardRules = fileTree(File(projectDir, "proguard")) {
include("*.pro")
}
debug {
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
proguardFiles("proguard-rules-debug.pro")
}
create("beta") {
lint {
abortOnError = true
fatal.add("StopShip")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
}
release {
lint {
abortOnError = true
fatal.add("StopShip")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
proguardFiles(*customProguardRules.toList().toTypedArray())
}
}
buildOutputs.all {
val variantOutputImpl = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
val variantName: String = variantOutputImpl.name
if (listOf("release", "beta").any { variantName.lowercase().contains(it) }) {
val outputFileName = ProjectConfig.packageName +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.uppercase()}.apk"
variantOutputImpl.outputFileName = outputFileName
}
}
buildFeatures {
viewBinding = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlin.time.ExperimentalTime",
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalUnsignedTypes",
"-Xannotation-default-target=param-property"
)
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
//noinspection WrongGradleMethod
tasks.withType<Test> {
useJUnitPlatform()
}
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
addBaseKotlin()
addDagger()
addMoshi()
addOkio()
addBaseAndroid()
addBaseAndroidUi()
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
addNavigation()
addBaseWorkManager()
addTesting()
"gplayImplementation"("com.android.billingclient:billing:8.0.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:8.0.0")
}
@@ -40,7 +40,7 @@ class UpgradeControlFoss @Inject constructor(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.DONATED
)
webpageTool.open("https://github.com/d4rken-org/capod#support-the-project")
webpageTool.open("https://github.com/sponsors/d4rken")
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
@@ -64,8 +64,11 @@ class UpgradeControlFoss @Inject constructor(
override val isPro: Boolean = false,
override val upgradedAt: Instant? = null,
val upgradeReason: FossUpgrade.Reason? = null,
override val error: Throwable? = null,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
}
override fun getSponsorUrl(): String = "https://github.com/sponsors/d4rken"
}
@@ -0,0 +1,16 @@
package eu.darken.capod.debug.autoreport
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class AutoReportingModule {
@Binds
@Singleton
abstract fun autoreporting(foss: FossAutoReporting): AutomaticBugReporter
}
@@ -0,0 +1,17 @@
package eu.darken.capod.debug.autoreport
import android.app.Application
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FossAutoReporting @Inject constructor() : AutomaticBugReporter {
override fun setup(application: Application) {
// NOOP
}
override fun notify(throwable: Throwable) {
throw IllegalStateException("Who initliazed this? Without setup no calls to here!")
}
}
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Skenk</string>
<string name="foss_upgrade_alreadydonated_label">Ek het reeds geskenk</string>
<string name="foss_upgrade_no_money_label">Ek spandeer al my geld op AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">ልገሳ</string>
<string name="foss_upgrade_alreadydonated_label">አስቀድሜ ልገሳ ፈጽሜአለሁ</string>
<string name="foss_upgrade_no_money_label">ገንዘቤን በሙሉ በAirPods ላይ አውጥቻለሁ</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">تبرُّع</string>
<string name="foss_upgrade_alreadydonated_label">أنا متبرع بالفعل</string>
<string name="foss_upgrade_no_money_label">أُنفق كل أموالي على AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Ахвяраваць</string>
<string name="foss_upgrade_alreadydonated_label">Я ўжо ахвяраваў</string>
<string name="foss_upgrade_no_money_label">Я патраціў усе грошы на AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Дарение</string>
<string name="foss_upgrade_alreadydonated_label">Вече съм дарил</string>
<string name="foss_upgrade_no_money_label">Харча всичките си пари за AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">দান করুন</string>
<string name="foss_upgrade_alreadydonated_label">আমি ইতিমধ্যে দান করেছি</string>
<string name="foss_upgrade_no_money_label">আমি আমার সব টাকা AirPods এর জন্য খরচ করি</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Doner</string>
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede doneret</string>
<string name="foss_upgrade_no_money_label">Jeg bruger alle mine penge på AirPods</string>
</resources>
+1 -1
View File
@@ -2,5 +2,5 @@
<resources>
<string name="foss_upgrade_donate_label">Spenden</string>
<string name="foss_upgrade_alreadydonated_label">Ich habe schon gespendet</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">Hab alles für AirPods ausgegeben</string>
<string name="foss_upgrade_no_money_label">Hab alles für AirPods ausgegeben</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Ya doné</string>
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Anneta</string>
<string name="foss_upgrade_alreadydonated_label">Juba annetasin</string>
<string name="foss_upgrade_no_money_label">Kulutasin kogu raha AirPodsidele</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Eman dohaintza</string>
<string name="foss_upgrade_alreadydonated_label">Dagoeneko eman dut dohaintza</string>
<string name="foss_upgrade_no_money_label">Nire diru guztia AirPods-etan gastatzen dut</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">کمک مالی</string>
<string name="foss_upgrade_alreadydonated_label">کمک مالی کرده‌ام</string>
<string name="foss_upgrade_no_money_label">من تمام پولم را خرج ایرپاد می‌کنم</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Lahjoita</string>
<string name="foss_upgrade_alreadydonated_label">Olen jo lahjoittanut</string>
<string name="foss_upgrade_no_money_label">Käytän kaikki rahani AirPodeihin</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Mag-donate</string>
<string name="foss_upgrade_alreadydonated_label">Nakapag-donate na ako</string>
<string name="foss_upgrade_no_money_label">Ginagastos ko lahat ng pera ko sa AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Xa doei</string>
<string name="foss_upgrade_no_money_label">Gasto todos os meus cartos en AirPods</string>
</resources>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">दान करना</string>
<string name="foss_upgrade_donate_label">दान करें</string>
<string name="foss_upgrade_alreadydonated_label">मैंने पहले ही दान कर दिया है</string>
<string name="foss_upgrade_no_money_label">मैं अपना सारा पैसा AirPods पर खर्च करता हूं</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Donirajte</string>
<string name="foss_upgrade_alreadydonated_label">Već sam donirao</string>
<string name="foss_upgrade_no_money_label">Sav svoj novac trošim na AirPods</string>
</resources>
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Adomány</string>
<string name="foss_upgrade_donate_label">Adományoz</string>
<string name="foss_upgrade_alreadydonated_label">Már adományoztam</string>
<string name="foss_upgrade_no_money_label">Minden pénzemet AirPodokra költöm</string>
<string name="foss_upgrade_no_money_label">Minden pénzemet az AirPods-ra költöttem</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Նվիրաբերել</string>
<string name="foss_upgrade_alreadydonated_label">Ես արդեն նվիրաբերել եմ</string>
<string name="foss_upgrade_no_money_label">Ես իմ ամբողջ գումարը ծախսում եմ AirPods-ի վրա</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Gefa</string>
<string name="foss_upgrade_alreadydonated_label">Ég hef þegar gefið</string>
<string name="foss_upgrade_no_money_label">Ég eyði öllum peningunum mínum í AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">寄付</string>
<string name="foss_upgrade_alreadydonated_label">すでに寄付しました</string>
<string name="foss_upgrade_no_money_label">AirPodsに全財産をつぎ込んでいます</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">შემოწირულობა</string>
<string name="foss_upgrade_alreadydonated_label">მე უკვე შემოწირული მაქვს</string>
<string name="foss_upgrade_no_money_label">მთელ ჩემს ფულს AirPods-ებზე ვხარჯავ</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">បរិច្ចាគ</string>
<string name="foss_upgrade_alreadydonated_label">ខ្ញុំបានបរិច្ចាគរួចហើយ</string>
<string name="foss_upgrade_no_money_label">ខ្ញុំចំណាយលុយទាំងអស់របស់ខ្ញុំលើ AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Beşdar bike</string>
<string name="foss_upgrade_alreadydonated_label">Min berê bexş kir</string>
<string name="foss_upgrade_no_money_label">Ez hemû pereyê xwe li AirPods xerc dikim</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">ದಾನ ಮಾಡಿ</string>
<string name="foss_upgrade_alreadydonated_label">ನಾನು ಈಗಾಗಲೇ ದಾನ ಮಾಡಿದ್ದೇನೆ</string>
<string name="foss_upgrade_no_money_label">ನಾನು ನನ್ನ ಎಲ್ಲಾ ಹಣವನ್ನು AirPods ಗಾಗಿ ಖರ್ಚು ಮಾಡುತ್ತೇನೆ</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Демөөрчүлүк кылуу</string>
<string name="foss_upgrade_alreadydonated_label">Мен буга чейин демөөрчүлүк кылгам</string>
<string name="foss_upgrade_no_money_label">Мен бүт акчамды AirPods үчүн короттум</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">ບໍລິຈາກ</string>
<string name="foss_upgrade_alreadydonated_label">ຂ້ອຍໄດ້ບໍລິຈາກແລ້ວ</string>
<string name="foss_upgrade_no_money_label">ຂ້ອຍໃຊ້ເງິນທັງໝົດຂອງຂ້ອຍໃສ່ AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Aukoti</string>
<string name="foss_upgrade_alreadydonated_label">Aš jau paaukojau</string>
<string name="foss_upgrade_no_money_label">Visus savo pinigus išleidžiu „AirPods“ ausinėms</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Ziedot</string>
<string name="foss_upgrade_alreadydonated_label">Es jau ziedoju</string>
<string name="foss_upgrade_no_money_label">Es iztērēju visu savu naudu AirPods austiņām</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Донирај</string>
<string name="foss_upgrade_alreadydonated_label">Веќе донирав</string>
<string name="foss_upgrade_no_money_label">Ги трошам сите пари на AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">സംഭാവന ചെയ്യുക</string>
<string name="foss_upgrade_alreadydonated_label">ഞാൻ ഇതിനകം സംഭാവന ചെയ്തു</string>
<string name="foss_upgrade_no_money_label">ഞാൻ എൻ്റെ എല്ലാ പണവും AirPods-നായി ചെലവഴിക്കുന്നു</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Хандивлах</string>
<string name="foss_upgrade_alreadydonated_label">Би аль хэдийн хандивласан</string>
<string name="foss_upgrade_no_money_label">Би бүх мөнгөө AirPods-д зарцуулдаг</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">दान करा</string>
<string name="foss_upgrade_alreadydonated_label">मी आधीच दान केले आहे</string>
<string name="foss_upgrade_no_money_label">मी माझे सर्व पैसे AirPods वर खर्च करतो</string>
</resources>
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Derma</string>
<string name="foss_upgrade_alreadydonated_label">Saya sudah menderma</string>
<string name="foss_upgrade_donate_label">Sumbang</string>
<string name="foss_upgrade_alreadydonated_label">Saya telah menyumbang</string>
<string name="foss_upgrade_no_money_label">Saya belanjakan semua wang saya untuk AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">လှူဒါန်းပါ</string>
<string name="foss_upgrade_alreadydonated_label">ကျွန်တော် လှူပြီးပါပြီ</string>
<string name="foss_upgrade_no_money_label">ကျွန်တော် ပိုက်ဆံအားလုံးကို AirPods မှာ သုံးတယ်</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">दान गर्नुहोस्</string>
<string name="foss_upgrade_alreadydonated_label">मैले पहिले नै दान गरिसकेको छु</string>
<string name="foss_upgrade_no_money_label">म आफ्नो सबै पैसा AirPods मा खर्च गर्छु</string>
</resources>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Dotacja</string>
<string name="foss_upgrade_donate_label">Wsparcie</string>
<string name="foss_upgrade_alreadydonated_label">Już się zrzuciłem</string>
<string name="foss_upgrade_no_money_label">Wydałem wszystkie pieniądze na AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Já doei</string>
<string name="foss_upgrade_no_money_label">Gastei todo o meu dinheiro em AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Donează</string>
<string name="foss_upgrade_alreadydonated_label">Am donat deja</string>
<string name="foss_upgrade_no_money_label">Mi-am cheltuit toți banii pe AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Dona</string>
<string name="foss_upgrade_alreadydonated_label">Apo giai donadu</string>
<string name="foss_upgrade_no_money_label">Apo spèndidu totu is soddos meos pro is AirPods</string>
</resources>
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">පරිත්‍යාග</string>
<string name="foss_upgrade_alreadydonated_label">මම දැනටමත් පරිත්‍යාග කර ඇත</string>
<string name="foss_upgrade_no_money_label">මම මගේ මුදල් සියල්ල AirPods සඳහා වියදම් කරමි</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Doniraj</string>
<string name="foss_upgrade_alreadydonated_label">Sem že doniral</string>
<string name="foss_upgrade_no_money_label">Ves svoj denar porabim za AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Dhuro</string>
<string name="foss_upgrade_alreadydonated_label">Unë tashmë kam dhuruar</string>
<string name="foss_upgrade_no_money_label">Unë i shpenzoj të gjitha paratë e mia për AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Донације</string>
<string name="foss_upgrade_alreadydonated_label">Већ сам донирао/ла</string>
<string name="foss_upgrade_no_money_label">Сав новац трошим на АирПодс</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Donera</string>
<string name="foss_upgrade_alreadydonated_label">Jag har redan donerat</string>
<string name="foss_upgrade_no_money_label">Jag spenderar alla mina pengar på AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Changia</string>
<string name="foss_upgrade_alreadydonated_label">Nimeshachangia tayari</string>
<string name="foss_upgrade_no_money_label">Natumia pesa zangu zote kwenye AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">நன்கொடை</string>
<string name="foss_upgrade_alreadydonated_label">நான் ஏற்கனவே நன்கொடை அளித்துவிட்டேன்</string>
<string name="foss_upgrade_no_money_label">எனது பணத்தை எல்லாம் ஏர்பாட்களில் செலவிடுகிறேன்</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">విరాళం ఇవ్వండి</string>
<string name="foss_upgrade_alreadydonated_label">నేను ఇప్పటికే విరాళం ఇచ్చాను</string>
<string name="foss_upgrade_no_money_label">నా డబ్బు అంతా ఎయిర్‌పాడ్స్‌కే ఖర్చు చేస్తాను</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">บริจาค</string>
<string name="foss_upgrade_alreadydonated_label">ฉันบริจาคแล้ว</string>
<string name="foss_upgrade_no_money_label">ฉันใช้เงินทั้งหมดไปกับ AirPods</string>
</resources>
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Bağış</string>
<string name="foss_upgrade_alreadydonated_label">Ben zaten bağışladım</string>
<string name="foss_upgrade_no_money_label">Tüm paramı AirPod\'lara harcıyorum</string>
<string name="foss_upgrade_donate_label">Bağış yap</string>
<string name="foss_upgrade_alreadydonated_label">Zaten bağış yaptım</string>
<string name="foss_upgrade_no_money_label">Tüm paramı AirPods\'a harcıyorum</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">عطیہ کریں۔</string>
<string name="foss_upgrade_alreadydonated_label">میں نے پہلے ہی عطیہ کر دیا ہے۔</string>
<string name="foss_upgrade_no_money_label">میں اپنی تمام رقم ایئر پوڈز پر خرچ کرتا ہوں۔</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Xayriya qilish</string>
<string name="foss_upgrade_alreadydonated_label">Men allaqachon xayriya qildim</string>
<string name="foss_upgrade_no_money_label">Men butun pulimni AirPods uchun sarflayman</string>
</resources>
+1 -1
View File
@@ -2,5 +2,5 @@
<resources>
<string name="foss_upgrade_donate_label">Quyên tặng</string>
<string name="foss_upgrade_alreadydonated_label">Tôi đã quyên góp</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">Tôi tiêu hết tiền cho AirPods</string>
<string name="foss_upgrade_no_money_label">Tôi tiêu hết tiền cho AirPods</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">資助</string>
<string name="foss_upgrade_alreadydonated_label">我已經資助了</string>
<string name="foss_upgrade_no_money_label">我已經為 AirPods 傾家蕩產了</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Nikela</string>
<string name="foss_upgrade_alreadydonated_label">Senginikele kakade</string>
<string name="foss_upgrade_no_money_label">Ngisebenzisa yonke imali yami kuma-AirPods</string>
</resources>
@@ -1,13 +0,0 @@
package eu.darken.capod.common.upgrade.core
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
import eu.darken.capod.common.upgrade.core.data.Sku
fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
PurchasedSku(Sku(it), this)
}
private val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Extensions")
@@ -17,14 +17,16 @@ import eu.darken.capod.common.upgrade.core.data.BillingData
import eu.darken.capod.common.upgrade.core.data.BillingDataRepo
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.pow
@Singleton
class UpgradeRepoGplay @Inject constructor(
@@ -49,25 +51,28 @@ class UpgradeRepoGplay @Inject constructor(
lastProStateAt = now
Info(billingData = data)
}
(now - lastProStateAt) < 6 * 60 * 1000L -> { // 6 hours
(now - lastProStateAt) < 6 * 60 * 60 * 1000L -> { // 6 hours
log(TAG, VERBOSE) { "We are not pro, but were recently, did GPlay try annoy us again?" }
Info(gracePeriod = true, billingData = null)
}
else -> {
Info(billingData = data)
}
}
}
.catch {
// Ignore Google Play errors if the last pro state was recent
.retryWhen { error, attempt ->
val now = System.currentTimeMillis()
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, error=$it" }
if ((now - lastProStateAt) < 6 * 60 * 60 * 1000L) { // 6 hours
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, attempt=$attempt, error=$error" }
if ((now - lastProStateAt) < 24 * 60 * 60 * 1000L) { // 24 hours
log(TAG, VERBOSE) { "We are not pro, but were recently, and just and an error, what is GPlay doing???" }
emit(Info(gracePeriod = true, billingData = null))
} else {
throw it
emit(Info(billingData = null, error = if (attempt == 0L) error else null))
}
delay(30_000L * 2.0.pow(attempt.toDouble()).toLong())
true
}
.replayingShare(scope)
@@ -117,13 +122,14 @@ class UpgradeRepoGplay @Inject constructor(
data class Info(
private val gracePeriod: Boolean = false,
private val billingData: BillingData?,
override val error: Throwable? = null,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type
get() = UpgradeRepo.Type.GPLAY
override val isPro: Boolean
get() = billingData?.getProSku() != null
get() = billingData?.getProSku() != null || gracePeriod
override val upgradedAt: Instant?
get() = billingData
@@ -1,7 +1,15 @@
package eu.darken.capod.common.upgrade.core.client
import android.app.Activity
import com.android.billingclient.api.*
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
@@ -19,17 +27,25 @@ data class BillingClientConnection(
) {
private val purchasesLocal = MutableStateFlow<Collection<Purchase>>(emptySet())
val purchases: Flow<Collection<Purchase>> = combine(purchasesGlobal, purchasesLocal) { global, local ->
val combined = mutableMapOf<String, Purchase>()
global.plus(local).toSet().sortedByDescending { it.purchaseTime }.forEach { purchase ->
combined[purchase.orderId] = purchase
}
combined.values
val combined = mutableSetOf<Purchase>()
combined.addAll(local)
global
.let { purchases -> purchases.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED } }
.let { combined.addAll(it) }
combined.sortedByDescending { it.purchaseTime }
}
.setupCommonEventHandlers(TAG) { "purchases" }
suspend fun queryPurchases(): Collection<Purchase> {
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
val (result: BillingResult, purchases) = suspendCoroutine<Pair<BillingResult, Collection<Purchase>?>> { continuation ->
client.queryPurchasesAsync(BillingClient.SkuType.INAPP) { result, purchases ->
client.queryPurchasesAsync(params) { result, purchases ->
continuation.resume(result to purchases)
}
}
@@ -38,7 +54,7 @@ data class BillingClientConnection(
if (!result.isSuccess) {
log(TAG, WARN) { "queryPurchases() failed" }
throw BillingClientException(result)
throw BillingResultException(result)
} else {
requireNotNull(purchases)
}
@@ -47,34 +63,32 @@ data class BillingClientConnection(
return purchases
}
suspend fun acknowledgePurchase(purchase: Purchase): BillingResult {
suspend fun acknowledgePurchase(purchase: Purchase) {
val ack = AcknowledgePurchaseParams.newBuilder().apply {
setPurchaseToken(purchase.purchaseToken)
}.build()
val ackResult = suspendCoroutine<BillingResult> { continuation ->
val result = suspendCoroutine<BillingResult> { continuation ->
client.acknowledgePurchase(ack) { continuation.resume(it) }
}
log(TAG) {
"acknowledgePurchase(purchase=$purchase): code=${ackResult.responseCode}, message=${ackResult.debugMessage})"
}
if (!ackResult.isSuccess) {
throw BillingClientException(ackResult)
}
log(TAG, INFO) { "acknowledgePurchase($purchase): code=${result.responseCode} (${result.debugMessage})" }
return ackResult
if (!result.isSuccess) throw BillingResultException(result)
}
suspend fun querySku(sku: Sku): Sku.Details {
val skuParams = SkuDetailsParams.newBuilder().apply {
setType(BillingClient.SkuType.INAPP)
setSkusList(listOf(sku.id))
val productDetails = QueryProductDetailsParams.Product.newBuilder().apply {
setProductType(BillingClient.ProductType.INAPP)
setProductId(sku.id)
}.build()
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<SkuDetails>?>> { continuation ->
client.querySkuDetailsAsync(skuParams) { skuResult, skuDetails ->
continuation.resume(skuResult to skuDetails)
val params = QueryProductDetailsParams.newBuilder().setProductList(listOf(productDetails)).build()
val (result, details) = suspendCoroutine<Pair<BillingResult, List<ProductDetails>>> { continuation ->
client.queryProductDetailsAsync(params) { billingResult, queryResult ->
val productDetailsList = queryResult.productDetailsList ?: emptyList()
continuation.resume(billingResult to productDetailsList)
}
}
@@ -82,9 +96,9 @@ data class BillingClientConnection(
"querySku(sku=$sku): code=${result.responseCode}, debug=${result.debugMessage}), skuDetails=$details"
}
if (!result.isSuccess) throw BillingClientException(result)
if (!result.isSuccess) throw BillingResultException(result)
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
if (details.isEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
return Sku.Details(sku, details)
}
@@ -97,10 +111,16 @@ data class BillingClientConnection(
suspend fun launchBillingFlow(activity: Activity, skuDetails: Sku.Details): BillingResult {
log(TAG) { "launchBillingFlow(activity=$activity, skuDetails=$skuDetails)" }
return client.launchBillingFlow(
activity,
BillingFlowParams.newBuilder().setSkuDetails(skuDetails.details.single()).build()
)
val productParams = BillingFlowParams.ProductDetailsParams.newBuilder().apply {
setProductDetails(skuDetails.details.first())
}.build()
val billingFlowParams = BillingFlowParams.newBuilder().apply {
setProductDetailsParamsList(listOf(productParams))
}.build()
return client.launchBillingFlow(activity, billingFlowParams)
}
companion object {
@@ -1,9 +1,11 @@
package eu.darken.capod.common.upgrade.core.client
import android.content.Context
import com.android.billingclient.api.BillingClient.*
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.newBuilder
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.Purchase
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.*
@@ -13,12 +15,15 @@ import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Singleton
class BillingClientConnectionProvider @Inject constructor(
@@ -29,7 +34,11 @@ class BillingClientConnectionProvider @Inject constructor(
val purchasePublisher = MutableStateFlow<Collection<Purchase>>(emptySet())
val client = newBuilder(context).apply {
enablePendingPurchases()
enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build()
)
setListener { result, purchases ->
if (result.isSuccess) {
log(TAG) {
@@ -44,36 +53,40 @@ class BillingClientConnectionProvider @Inject constructor(
}
}.build()
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
log(TAG, VERBOSE) { "startConnection(...)" }
client.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
log(TAG, VERBOSE) {
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
log(TAG, VERBOSE) { "startConnection(...)" }
client.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
log(TAG, VERBOSE) {
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
}
when (result.responseCode) {
BillingResponseCode.OK -> {
val connection = BillingClientConnection(client, purchasePublisher)
trySendBlocking(connection)
launch {
try {
purchasePublisher.value = connection.queryPurchases()
log(TAG) { "Initial IAP query successful." }
} catch (e: Exception) {
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
}
}
}
else -> {
close(BillingResultException(result))
}
continuation.resume(result)
}
}
override fun onBillingServiceDisconnected() {
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
close(CancellationException("Billing service disconnected"))
}
})
}
val billingClientConnection = when (connectionResult.responseCode) {
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
else -> throw BillingClientException(connectionResult)
}
try {
purchasePublisher.value = billingClientConnection.queryPurchases()
log(TAG) { "Initial IAP query successful." }
} catch (e: Exception) {
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
}
send(billingClientConnection)
override fun onBillingServiceDisconnected() {
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
close(BillingException("Billing service disconnected"))
}
})
log(TAG) { "Awaiting close." }
awaitClose {
@@ -85,26 +98,28 @@ class BillingClientConnectionProvider @Inject constructor(
val connection: Flow<BillingClientConnection> = connectionProvider
.setupCommonEventHandlers(TAG) { "connection" }
.retryWhen { cause, attempt ->
log(TAG) { "Billing client connection error: ${cause.asLog()}" }
if (cause is CancellationException) {
log(TAG) { "BillingClient connection cancelled." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
if (cause !is BillingException) {
log(TAG, WARN) { "Unknown exception type: $cause" }
return@retryWhen false
}
if (cause !is BillingClientException) {
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
return@retryWhen false
} else {
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
}
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
return@retryWhen false
}
log(TAG) { "Will retry BillingClient connection... *sigh*" }
delay(3000 * attempt)
true
@@ -1,11 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import com.android.billingclient.api.BillingResult
class BillingClientException(val result: BillingResult) : Exception() {
override val message: String?
get() = result.debugMessage
override fun toString(): String =
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
}
@@ -4,4 +4,14 @@ import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingResult
internal val BillingResult.isSuccess: Boolean
get() = responseCode == BillingClient.BillingResponseCode.OK
get() = responseCode == BillingClient.BillingResponseCode.OK
internal val BillingResult.isGplayUnavailableTemporary: Boolean
get() = setOf(
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
BillingClient.BillingResponseCode.SERVICE_TIMEOUT
).contains(responseCode)
internal val BillingResult.isGplayUnavailablePermanent: Boolean
get() = responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
@@ -0,0 +1,15 @@
package eu.darken.capod.common.upgrade.core.client
import android.content.Context
import eu.darken.capod.R
import eu.darken.capod.common.error.HasLocalizedError
import eu.darken.capod.common.error.LocalizedError
open class BillingException(override val message: String) : Exception(), HasLocalizedError {
override fun getLocalizedError(context: Context): LocalizedError = LocalizedError(
throwable = this,
label = context.getString(R.string.upgrades_gplay_billing_error_label),
description = context.getString(R.string.upgrades_gplay_billing_error_description, message)
)
}
@@ -0,0 +1,18 @@
package eu.darken.capod.common.upgrade.core.client
import android.content.Context
import com.android.billingclient.api.BillingResult
import eu.darken.capod.R
import eu.darken.capod.common.error.LocalizedError
class BillingResultException(val result: BillingResult) : BillingException(result.debugMessage) {
override fun toString(): String =
"BillingResultException(code=${result.responseCode}, message=${result.debugMessage})"
override fun getLocalizedError(context: Context): LocalizedError = LocalizedError(
throwable = this,
label = context.getString(R.string.upgrades_gplay_billing_result_error_label),
description = context.getString(R.string.upgrades_gplay_billing_result_error_description, result)
)
}
@@ -7,13 +7,9 @@ import eu.darken.capod.common.error.LocalizedError
class GplayServiceUnavailableException(cause: Throwable) : Exception("Google Play services are unavailable.", cause),
HasLocalizedError {
override fun getLocalizedError(context: Context): LocalizedError {
return LocalizedError(
throwable = this,
label = "Google Play Services Unavailable",
description = context.getString(R.string.upgrades_gplay_unavailable_error)
)
}
override fun getLocalizedError(context: Context): LocalizedError = LocalizedError(
throwable = this,
label = "Google Play Services Unavailable",
description = context.getString(R.string.upgrades_gplay_unavailable_error)
)
}
@@ -1,11 +1,14 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.upgrade.core.toPurchasedSku
data class BillingData(
val purchases: Collection<Purchase>
) {
val purchasedSkus: Collection<PurchasedSku>
get() = purchases.map { it.toPurchasedSku() }.flatten()
private fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
PurchasedSku(Sku(it), this)
}
}
@@ -1,8 +1,6 @@
package eu.darken.capod.common.upgrade.core.data
import android.app.Activity
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.*
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.*
@@ -11,9 +9,7 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.replayingShare
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.upgrade.core.client.BillingClientConnectionProvider
import eu.darken.capod.common.upgrade.core.client.BillingClientException
import eu.darken.capod.common.upgrade.core.client.GplayServiceUnavailableException
import eu.darken.capod.common.upgrade.core.client.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -54,37 +50,34 @@ class BillingDataRepo @Inject constructor(
}
.forEach {
log(TAG, INFO) { "Acknowledging purchase: $it" }
try {
client.acknowledgePurchase(it)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to ancknowledge purchase: $it\n${e.asLog()}" }
}
client.acknowledgePurchase(it)
}
}
.setupCommonEventHandlers(TAG) { "connection-acks" }
.retryWhen { cause, attempt ->
log(TAG, ERROR) { "Failed to acknowledge purchase: ${cause.asLog()}" }
if (cause is CancellationException) {
log(TAG) { "Ack was cancelled (appScope?) cancelled." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
return@retryWhen false
}
if (cause !is BillingClientException) {
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
if (cause !is BillingException) {
log(TAG, WARN) { "Unknown exception type: $cause" }
return@retryWhen false
} else {
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
}
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
log(TAG) { "Got BILLING_UNAVAILABLE while trying to ACK purchase." }
return@retryWhen false
}
log(TAG) { "Will retry ACK" }
log(TAG) { "Will retry ACK (attempt=$attempt)" }
delay(3000 * attempt)
true
}
@@ -109,7 +102,7 @@ class BillingDataRepo @Inject constructor(
} catch (e: Exception) {
log(TAG, WARN) { "Failed to start IAP flow:\n${e.asLog()}" }
val ignoredCodes = listOf(3, 6)
if (e !is BillingClientException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
if (e !is BillingResultException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
Bugs.report(TAG, "IAP flow failed for $sku", e)
}
@@ -120,16 +113,14 @@ class BillingDataRepo @Inject constructor(
companion object {
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "DataRepo")
internal fun Throwable.tryMapUserFriendly(): Throwable {
if (this !is BillingClientException) return this
return when (result.responseCode) {
BillingResponseCode.BILLING_UNAVAILABLE,
BillingResponseCode.SERVICE_UNAVAILABLE,
BillingResponseCode.SERVICE_DISCONNECTED,
BillingResponseCode.SERVICE_TIMEOUT -> GplayServiceUnavailableException(this)
else -> this
internal fun Throwable.tryMapUserFriendly(): Throwable = when {
this is BillingResultException && this.result.isGplayUnavailableTemporary -> {
GplayServiceUnavailableException(this)
}
this is BillingResultException && this.result.isGplayUnavailablePermanent -> {
GplayServiceUnavailableException(this)
}
else -> this
}
}
}
@@ -1,7 +1,6 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.upgrade.core.data.Sku
data class PurchasedSku(val sku: Sku, val purchase: Purchase) {
override fun toString(): String = "IAP(sku=$sku, purchase=${purchase.skus})"
@@ -1,12 +1,12 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.SkuDetails
import com.android.billingclient.api.ProductDetails
data class Sku(
val id: String
) {
data class Details(
val sku: Sku,
val details: Collection<SkuDetails>,
val details: Collection<ProductDetails>,
)
}
@@ -0,0 +1,16 @@
package eu.darken.capod.debug.autoreport
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class AutoReportingModule {
@Binds
@Singleton
abstract fun autoreporting(foss: GplayAutoReporting): AutomaticBugReporter
}
@@ -0,0 +1,42 @@
package eu.darken.capod.debug.autoreport
import android.app.Application
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.InstallId
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GplayAutoReporting @Inject constructor(
@ApplicationContext private val context: Context,
private val debugSettings: DebugSettings,
private val installId: InstallId,
) : AutomaticBugReporter {
override fun setup(application: Application) {
val isEnabled = debugSettings.isAutoReportingEnabled.value
log(TAG) { "setup(): isEnabled=$isEnabled" }
if (!isEnabled) return
// Currently no 3rd party bug tracking
Bugs.reporter = this
}
override fun notify(throwable: Throwable) {
log(TAG, WARN) { "notify($throwable)" }
}
companion object {
private val TAG = logTag("Debug", "AutoReport")
}
}
+4 -1
View File
@@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="upgrades_gplay_unavailable_error">Google Play-dienste is nie beskikbaar nie.</string>
<string name="upgrades_no_purchases_found_check_account">Geen aankope gevind nie. Gebruik jy die regte rekening?</string>
</resources>

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