Compare commits

..
157 Commits
Author SHA1 Message Date
darken f27d84fec3 Release: 5.1.0-rc0 2026-04-17 21:59:40 +02:00
darken 7632ba9e0a fix(widget): Complete placement after upgrade purchase 2026-04-17 21:58:05 +02:00
darken fd20ef0500 fix(bluetooth): Keep connectedDevices retrying while BLUETOOTH_CONNECT is missing 2026-04-17 21:48:55 +02:00
darken 250d0ab154 fix(monitor): Gate AAP lifecycle on BLUETOOTH_CONNECT permission 2026-04-17 21:48:55 +02:00
darken 79886e0bfb General: Update app translations from Crowdin 2026-04-17 21:19:24 +02:00
darken 710d443074 General: Update fastlane translations from Crowdin 2026-04-17 21:19:24 +02:00
darken 86b685f28b feat(widget): Add ANC control widget
New home-screen widget that toggles AirPods ANC modes (Off / ANC / Transparency / Adaptive) directly, with six adaptive layouts that pick based on widget size (QUAD_CORNERS, ROW_ICONS, COLUMN_ICONS, GRID_2X2, ROW, COLUMN). Consolidated battery+ANC configuration into a single ViewModel that detects widget type from AppWidgetManager. ACTIVE state uses Material3 secondaryContainer/onSecondaryContainer for guaranteed contrast. Includes live config preview, preview subtitle, stale-selection guard, device-label toggle, and aligned icons with the app's AncModeSelector.
2026-04-17 20:10:48 +02:00
darken bba9155573 feat(press-controls): Add media and noise-control actions
Adds Stop, Fast Forward, Rewind, Mute, Cycle Noise Control, and Toggle Transparency as gesture targets. Migrates StemAction from enum to sealed interface with polymorphic serialization to unlock future parameterized actions. ANC actions are gated per-device capability and reuse the existing listening-mode cycle mask.
2026-04-17 17:56:31 +02:00
darken c19b0b35b6 chore(claude): Ignore scheduled_tasks.lock 2026-04-17 15:26:05 +02:00
darken ec087e4b07 feat(device-settings): Split stem actions into dedicated Press Controls screen
Move press timing, call controls, and stem mappings out of the Controls card into a new Press Controls screen. The screen name is device-agnostic so it applies to both stemmed AirPods and the AirPods Max's Digital Crown/noise button.

Stem mappings are now per-device (stored on the AppleDeviceProfile) rather than a single app-global DataStore. The Pro gate for mappings moves from screen entry to per-change with an Upgrade badge on the mappings card header, so free users can still access the non-Pro press timing and call-control settings that live on the same screen.
2026-04-17 15:26:05 +02:00
darken e7c280cb52 ui(overview): Add title icons to permission and device-limit cards 2026-04-17 13:02:14 +02:00
darken 8a3a6cade5 ui(device-settings): Remove Edit/Upgrade labels from listening mode cycle row 2026-04-17 13:02:14 +02:00
darken 77486fcd48 fix(overview): Make device limit upgrade card full width 2026-04-17 13:02:14 +02:00
darken f4ec4630e4 chore(upgrade): Bump foss upgrade cache key 2026-04-17 13:02:14 +02:00
darken 384d81be18 feat(aap): Persist learned ANC settings across reconnects 2026-04-17 13:02:14 +02:00
darken 07b3b95270 refactor(device-settings): Extract section cards into own files 2026-04-17 13:02:14 +02:00
darken 5b187c6d5b refactor(device-settings): Extract setting controls into own files 2026-04-17 13:02:14 +02:00
darken 5ee094da3c feat(device-settings): Split ANC-with-one-pod into its own toggle 2026-04-17 13:02:14 +02:00
darken 77ea31216f feat(overview): Sort and collapse inactive device cards
Sort profiled devices into tiers (system-connected > nearby > cached), ordered by profile list within each tier. Collapse non-pinned cards to a compact battery tray with mini gauge rings matching the expanded card design. System-connected and top devices stay expanded; others can be tapped to toggle.
2026-04-17 13:02:14 +02:00
darken 74e48e79eb Refactor packages 2026-04-17 13:02:14 +02:00
darken f141ce3f2a refactor(aap): Extract controllers and centralize timer lifecycle
Split AapSessionEngine into dedicated controllers (AapAncController, AapOutboundController), a typed inbound decoder (AapInboundInterpreter), a HID frame batcher (HidTracker), and a device-info diagnostics helper (AapDeviceInfoDiagnostics). Each controller returns typed decisions carrying state, timer actions, and logs instead of mutating engine state via callbacks. AapSettingsCoordinator is now stateless — pending queue and verification state live in engine runtime state.

All coroutine timer Jobs live in the engine's timerJobs map keyed by EngineTimerKey, with cancelAllTimers() on reset to prevent forgotten cancellations. Engine event dispatch is split: suspend path for user-initiated sends (errors propagate to caller), non-suspend for sync events (timer fires, inbound updates).
2026-04-16 10:32:21 +02:00
darken 14da81d231 fix(device-settings): Fix ANC OFF mode visibility and inference logic
Fix Elvis operator precedence bug in visibleAncModes() where OFF passed the filter unconditionally. Move filtering logic to PodDevice.visibleAncModes extension, unifying DualPodsCard, SinglePodsCard, and DeviceSettingsScreen. Gate AllowOffOption inference on pod-in-ear + 1.5s stability to prevent false positives from in-case OFF reports.
2026-04-16 10:32:21 +02:00
darken 789c23d24c feat(aap): Add structured HID descriptor logging with batched summaries
During case transitions, AirPods send 800+ cmd 0x0017 HID frames in ~20s. Previously each logged identically at VERBOSE with raw hex (~160KB noise). Now a HidTracker classifies frames (service directory, descriptor bulk, terminator) and batches consecutive bulk frames by (phase, fill), emitting 3-4 summary lines instead of 822.
2026-04-16 10:32:21 +02:00
darken 4ba59636c0 fix(aap): Fix flush ANC debounce, HANDSHAKING→READY regression, and flush ordering
Track lastAncSentAt separately so non-ANC commands during flush don't break the ANC echo debounce window. Prioritize ANC for post-flush verification.

Move HANDSHAKING→READY transition to top of processMessage so decoded battery, stem press, and device info messages also trigger it.

Sort flush: AllowOffOption before AncMode before others, preventing device rejection when enabling OFF mode.
2026-04-16 10:32:21 +02:00
darken e5821de4dd feat(aap): Unified command queue with ear-detection gating and session engine extraction
Queue all setting commands when no pod is in ear, flush when a pod goes in. Enable the adaptive noise slider when ADAPTIVE mode is pending. Show an info box when settings changes are pending.

Extract AapSessionEngine (state, send path, message processing, inference) and AapSettingsCoordinator (queue, optimistic updates, verification) from AapConnection, reducing it from 773 to 173 lines.
2026-04-16 10:32:21 +02:00
darken 59b4c404aa fix(device-settings): Use italic instead of cursive for name mismatch highlight 2026-04-16 10:32:21 +02:00
darken 0d566a5aec feat(device-settings): Inline edit icon next to device label, make label tappable 2026-04-16 10:32:21 +02:00
darken 35c06d9982 feat(device-settings): Move device details to bottom sheet
Move serial, firmware, build, manufacturer, and per-pod serials from

the info card into a ModalBottomSheet triggered by an info icon.

Firmware+build and left+right pod serials render as paired rows.
2026-04-16 10:32:21 +02:00
darken 3ad732ff09 feat(device-settings): Enrich device info card, extract shared ANC mode UI
Device info card: add model label with Apple model number, rename label to

Bluetooth Device Label, show cursive font on name mismatch with system BT

name, combine first/last seen on single row, add profile prefix to subtitle.

ANC mode: extract shared AncModeUi helpers for labels and icons, update

overview cards and device settings to use shared components.
2026-04-16 10:32:21 +02:00
darken 32ff7e5bdd feat(aap): Decode unknown settings, extend DeviceInfo, fix UTF-8 parsing
- Add UnknownSetting catch-all for 9 unconfirmed H2+ setting IDs (0x29, 0x2C, 0x2F, 0x30, 0x33, 0x37, 0x38, 0x3B, 0x3E)

- Downgrade known non-settings commands (0x0000, 0x0002, 0x000C, 0x0017, 0x002B, 0x004E, 0x0055, 0x0057) from INFO to VERBOSE

- Add earbud serials and build number to DeviceInfo (parsed from 0x001D segments 8-10, shown in DeviceInfoCard, persisted in cache)

- Fix UTF-8 device name parsing (curly quotes etc. no longer break segment indices)

- Add [was: ...] to setting change logs for easier protocol diff analysis

- Label DeviceInfoDump segments 5-12 (firmwareVersionDup, protocolVersion, updaterAppId, earbudSerials, buildNumber, encryptedBlob, timestamp)
2026-04-16 10:32:21 +02:00
darken fd6a9667e2 fix(aap): Correct EndCallMuteMic mapping based on real-device behavior
Swap the logical mapping for `EndCallMuteMic` settings to align with hardware states observed on AirPods Pro devices. Updated session tests for Pro 1, 2, and 3 to reflect the corrected protocol behavior.
2026-04-16 10:32:21 +02:00
darken c5d9bed5f0 feat(device-settings): Enable speculative feature flags for older models
Based on Apple's official documentation, enable features for models

that support them per iOS but were previously excluded in CAPod:

- Gen 1/2: microphone mode (auto/left/right)

- Gen 3: press speed, press hold duration, tone volume, microphone mode

- Gen 4 non-ANC: press speed, press hold duration, tone volume

- Max original/USB-C: listening mode cycle, allow off option
2026-04-16 10:32:21 +02:00
darken 90af17bf7d feat(device-settings): Add experimental info box, revise pro-gating, fix switch bypass
Add SettingsInfoBox title support and experimental feature warning for Sleep Detection and Personalized Volume toggles with issue tracker action.

Pro-gate tone volume, microphone mode. Un-gate sleep detection, conversation awareness. Fix SettingsSwitchItem allowing direct switch toggle to bypass requiresUpgrade.
2026-04-16 10:32:21 +02:00
darken 6f90687b72 refactor(aap): Send InitExt unconditionally to all devices
Older devices silently ignore the 0x4D packet, so there is no need to gate it per model. Sending it unconditionally prevents silent feature degradation when new H2+ models are added without the flag.

Remove needsInitExt from PodModel.Features.
2026-04-15 08:07:12 +02:00
darken f17b6ca99f feat(device-settings): Merge one-pod mode settings into a single toggle
Combine reaction one-pod mode (autoplay/autopause) and firmware NC-with-one-AirPod into one setting. The merged toggle always sets the app-local flag and additionally syncs the firmware NC setting via AAP when connected.

Also remove redundant @OptIn(ExperimentalCoroutinesApi::class) annotations from tests, already covered by the global compiler opt-in flag.
2026-04-15 07:37:51 +02:00
darken 397b32b425 fix(aap): Update feature flags for Max 2 and Powerbeats Pro 2, sync ear detection
Add H2-chip feature flags (adaptive ANC, conversation awareness, personalized volume, listening mode cycle, InitExt) to AirPods Max 2. Add sleep detection and ear detection toggle to Powerbeats Pro 2.

Auto-sync device ear detection setting when auto-play/auto-pause reactions are toggled. Add flag invariant tests to prevent future drift.
2026-04-15 06:47:05 +02:00
darken 4b80ac7336 feat(overview): Add dynamic Bluetooth status icon to toolbar 2026-04-15 06:46:54 +02:00
Matthias Urhahn 5d5e6670d4 fix(settings): Improve AAP unavailable description with update guidance (#513) 2026-04-14 19:23:32 +02:00
darken da64406638 feat(device-settings): Show Auto Connect caveat as conditional info box 2026-04-14 19:21:34 +02:00
darken 77b8992355 refactor(aap): Enrich unhandled message log with payload and send context 2026-04-14 18:15:22 +02:00
darken f3b540987d fix(aap): Use compact write format for EndCallMuteMic
Real AirPod firmware silently drops writes with the LibrePods-documented 0x21 subtype. Switch to the compact 0x20 format that matches what devices actually emit in their echo frames. Also enforce the complementary-pair invariant at the SetEndCallMuteMic command boundary so validation runs before the optimistic UI update.
2026-04-14 17:38:08 +02:00
darken 1a0bfbb3f4 refactor(aap): Log outgoing AAP commands with raw hex at VERBOSE
Mirrors the existing incoming MSG log so a full transcript can verify round-trips for settings the device doesn't echo back.
2026-04-14 16:07:29 +02:00
darken 02fd83440b fix(test): Update AdaptiveAudioNoise tests for inverted wire semantics
The encode/decode now flips UI 0..100 to wire 100..0. Update the clamp and decode expectations accordingly, and add a round-trip test to guard against asymmetric changes in either direction.
2026-04-14 15:57:07 +02:00
darken 7f82119001 fix(aap): Correct Adaptive noise capability flags and level semantics
Switch the 0x4D init packet flags from 0x0E to 0xD7 to match the value captured from Apple's own stack (librepods AACPManager). The previous value traced to MagicPodsCore and matches no known Apple capture.

Invert the Adaptive Audio Noise level on write and read: wire 0 means max noise reduction, wire 100 means transparency-like. UI stays in intuitive 100 = max NC semantics, matching librepods' slider behavior.
2026-04-14 15:57:07 +02:00
darken 8229163f70 fix(settings): Gate Adaptive Audio Noise slider on Adaptive noise mode 2026-04-14 15:57:07 +02:00
darken 6d313c077d feat(stem): Note that long-press overrides native ANC cycling 2026-04-14 14:44:22 +02:00
darken 60970a2a0c refactor(device-settings): Group Reactions items with dividers
Three dividers separate the logical clusters inside the Reactions section: ear-detection behaviors, AAP auto-behaviors (when connected), connection settings, and pop-up notifications. AAP divider only renders when at least one AAP item is shown.
2026-04-14 14:39:01 +02:00
darken c28502fc8a refactor(device-settings): Fold Conversation Awareness and Sleep Detection into Reactions
AAP vs BLE is an implementation detail. From a user perspective these are 'when X happens, do Y' toggles like Auto Play/Pause. Gate them on device.isAapConnected so they stay hidden on phones without L2CAP support, avoiding confusion. The separate 'Smart features' section and its string resource are removed.
2026-04-14 14:39:01 +02:00
darken 2f3cae2b2c refactor(device-settings): Reorganize sections and extract components
Visual: wrap Noise Control in SettingsSection for consistency with Sound/Controls/Smart features, add divider between ANC mode picker and trailing items.

Categories: rename 'Other' to 'Smart features', move Microphone Mode to Sound, move Conversation Awareness to Smart features alongside Sleep Detection, place Adaptive Noise adjacent to ANC mode picker.

Extraction: split DeviceInfoCard, NotConnectedCard, AapUnavailableCard into cards/; AutoConnectConditionDialog, RenameDialog, SystemRenameUnavailableDialog into dialogs/; NoiseControlCombined into components/. Each with isolated previews.
2026-04-14 14:39:01 +02:00
darken 566a6cd120 feat(settings): Replace rename-failure snackbar with dialog 2026-04-14 11:38:09 +02:00
darken 7d08a44a19 refactor: Standardize DeviceSettingsViewModelTest with runTest and explicit VM cleanup 2026-04-14 11:28:41 +02:00
darken dae12e588f fix(test): Drop shared scheduler context from runTest2 calls 2026-04-14 11:28:41 +02:00
darken 6d5c5d29cc fix(test): Use runTest2 with autoCancel for DeviceSettingsViewModelTest 2026-04-14 11:28:41 +02:00
darken bac970fa61 fix(l10n): Remove extra translations missing from default locale 2026-04-14 11:28:41 +02:00
darken 6e8439e2f4 fix: Add missing WARN import in AppleFactory 2026-04-14 11:28:41 +02:00
darken df0fcede6f fix(logging): Reduce PlayPause diagnostic noise
Remove Pre-distinct log that fired on every emission. Replace full PodDevice dump in Checking log with compact ear detection summary.
2026-04-14 11:28:41 +02:00
darken d4a355c356 fix(reaction): Fix autoplay/autopause for AAP-based ear detection
PlayPause coerced null per-side ear values to false, making all ear states invisible when resolvedPrimaryPod was unknown. Fall back to AAP aggregate state (isBeingWorn/isEitherPodInEar) when per-side mapping is unavailable, keeping PodDevice.isLeftInEar/isRightInEar truthful for UI consumers. Add isEitherPodInEar to PlayPauseMonitorKey for proper dedup. Add diagnostic logging at the distinctUntilChangedBy boundary.
2026-04-14 11:28:41 +02:00
darken 200e32f67a fix(logging): Promote user-triggered action logs to INFO level 2026-04-14 11:28:41 +02:00
darken c0060ce5ca feat(popup): Suppress popup when app is visible and add settings hints
Hide the overlay popup when MainActivity is in the foreground since the user can already see battery info in the app. Show an info card when popups are enabled explaining they only appear outside the app. Show a warning card with a Fix button when monitor mode is MANUAL.

Refactor SettingsInfoBox into a reusable component with INFO/WARNING types and optional action slot.
2026-04-14 11:28:41 +02:00
darken 34c23751b3 refactor(ui): Split PodCardComponents into per-component files
Move each composable from PodCardComponents.kt into its own file under a new cards/components/ subpackage. Add previews to each file. Also add getBatteryIcon() for Compose Material Icon battery levels used in the popup.
2026-04-14 11:28:41 +02:00
darken 04c23ecf63 fix(ble): Prefer case-context pod when merging split BLE broadcasts
When both pods broadcast independently (one in case, one on desk), the pod inside the case carries authoritative case state via hasCaseContext bits. Previously, whichever address was processed last would overwrite the other, causing case state to flip-flop between OPEN and NOT_IN_CASE every scan cycle.

Two-layer fix: BlePodMonitor.processWithCache() now prefers the pod with case context when two scan results map to the same identity in one batch. ApplePodsFactory.getLatestCaseLidState() no longer treats NOT_IN_CASE as authoritative when recent history contains a broadcast with case context.
2026-04-14 11:28:41 +02:00
darken b1aee5ee83 fix(popup): Show charging indicator in connection popup 2026-04-14 11:28:41 +02:00
darken 2a8416d146 fix(settings): Fix OFF visibility toggle not reacting on first press
Serialize AapConnection.send() with a dedicated sendMutex so two concurrent optimistic state updates cannot clobber each other. Collapse the OFF toggle click into a single combined VM call that runs both SetListeningModeCycle and SetAllowOffOption sequentially.
2026-04-14 11:28:41 +02:00
darken 1f7bc7cfd8 fix(popup): decode lid state from case context 2026-04-14 11:28:41 +02:00
darken 46fcf5a4cb fix(popup): use fresh broadcast timing for connection popup 2026-04-14 11:28:41 +02:00
darken a4e7820e6f Add delayed retry for permission recheck when returning from settings 2026-04-14 11:28:41 +02:00
darken e6e66c3831 fix(logging): Demote noisy BLE logs and redact summaries 2026-04-14 11:28:41 +02:00
darken 71ba531025 Break cache feedback loops in monitor and reactions 2026-04-14 11:28:41 +02:00
darken 6825abaa41 Guard BLE-only autoplay and clarify one-pod mode 2026-04-14 11:28:41 +02:00
darken a4e1a3ba2e fix(overview): Hide stale cached age on live cards 2026-04-14 11:28:41 +02:00
darken 1cd8ae5ace Refactor time access behind TimeSource 2026-04-14 11:28:41 +02:00
darken c1601c105c Fix auto-resume after CAP pause 2026-04-14 11:28:41 +02:00
darken b8f342f096 Resolve live pod profiles from current repo state 2026-04-14 11:28:41 +02:00
darken a6f292f737 Render device settings before BT profile state arrives 2026-04-14 11:28:41 +02:00
darken a37b9b0213 fix(settings): Show passive info when device is connected but AAP unavailable 2026-04-14 11:28:41 +02:00
darken 65a74e9bc1 feat(settings): Group device settings into card containers
Wrap setting sections in SettingsSection cards using Material 3 surfaceContainerLow surfaces with rounded corners. Reorder Controls by usage frequency, move Microphone Mode to Other section, and replace ear detection info row with a contextual info box that only appears for BLE-only connections.
2026-04-14 11:28:41 +02:00
darken 609423b27d fix: Force single line for UpgradeBadge text 2026-04-14 11:28:41 +02:00
darken 067eb2e9e0 feat(reactions): Move reaction settings to per-device profiles
Migrate reaction toggles (auto-play, auto-pause, auto-connect, popups) from global ReactionSettings singleton to per-profile fields on AppleDeviceProfile. Each paired device can now have independent reaction behavior.

Extract ReactionConfig snapshot to decouple PodDevice from AppleDeviceProfile — reaction consumers read device.reactions instead of device.profile. Remove auto-connect from upgrade benefits (now free). Add LegacyReactionSettingsReader for one-shot DataStore migration.
2026-04-14 11:28:41 +02:00
darken 29df90628f fix: Handle ForegroundServiceDidNotStartInTimeException crash 2026-04-14 11:24:49 +02:00
darken 70d9c047dc fix: Prevent duplicate device cards for stranger AirPods with same model 2026-04-13 14:09:33 +02:00
darken 7f2b6976ce fix(upgrade): Replace TopAppBar with floating back button to prevent mascot clipping 2026-04-13 14:09:16 +02:00
darken 37e2593d8a fix(overview): Warn when profile has no paired Bluetooth device 2026-04-11 17:56:21 +02:00
darken 30163a3f69 feat(settings): Add flavor-specific label to upgrade badge
Makes the inline upgrade badge a tiny star + short label (Pro on gplay, FOSS on foss) instead of icon-only. Text is flavor-switched via upgrade_badge_label in each flavor's strings.xml, primary color, labelSmall typography, no pill background to keep it lightweight when multiple rows stack it. Accessibility uses the existing common_upgrade_required_label.
2026-04-11 17:03:40 +02:00
darken ed6c939284 refactor(settings): Rename ProBadge to UpgradeBadge for flavor neutrality
The 'Pro' label is gplay-specific vocabulary — FOSS unlocks the same features via sponsorship. Aligns with the existing flavor-neutral terms already used in UpgradeRepo, Nav.Main.Upgrade and launchUpgrade(). Renames the proLocked parameter to requiresUpgrade across SettingsBaseItem and its three wrappers, and adds a new common_upgrade_required_label string (Requires upgrade) for the badge content description.
2026-04-11 17:03:40 +02:00
darken a51b24f284 feat(settings): Show pro indicators on gated settings rows
Unifies the three inconsistent pro-gating UI patterns (text button header, star-replaces-switch, silent gates) into a single inline star badge next to the title, driven by a new proLocked parameter on SettingsBaseItem that propagates to all wrappers. Switches stay visible so users can see state and disable ex-pro toggles. Hides the Noise Control visibility buttons when non-pro and surfaces a dedicated cycle customization row with the indicator. Deletes two duplicate local ProGated composables.
2026-04-11 17:03:40 +02:00
darken 09c0692205 refactor(overview): Thread isPro/onUpgrade into pod cards (groundwork)
Groundwork for a future pro indicator on the overview card's ANC mode selector. Adds isPro + onUpgrade parameters to OverviewScreen, PodDeviceCard, DualPodsCard, SinglePodsCard (currently unused inside the cards, defaults keep existing previews working). Wraps AncModeSelector in a Box as a scaffold for an overlay. Bumps surfaceContainerLowDark to match surfaceContainerDark in all three theme palettes (Amber/Blue/Green) across standard/medium/high contrast. No user-facing change yet.
2026-04-11 15:01:47 +02:00
darken 4174072dbf docs: Update supported devices list in README 2026-04-10 08:18:21 +02:00
RomaZinkevich aeaf92d7e6 support for airpods max 2 added 2026-04-10 07:58:46 +02:00
darken a650405c2d chore: Bump Android Gradle Plugin to 9.1.0 2026-04-09 07:47:56 +02:00
darken 7883dcbafb Release: 5.0.1-rc0 2026-04-08 20:27:19 +02:00
darken 300e6b8c49 chore: .gitignore 2026-04-08 20:25:13 +02:00
darken 33b6fbd0e0 General: Update app translations from Crowdin
Pull latest string translations from Crowdin. Validated and reverted any problematic changes.
2026-04-08 20:24:34 +02:00
darken f2bf39bda1 fix(monitor): Use AAP message time for Last Seen while connected 2026-04-08 19:21:19 +02:00
darken 87d366bcb7 refactor(ui): Improve device settings layout and grouping 2026-04-08 19:09:59 +02:00
darken df5732569f docs: Add Crowdin to README acknowledgments 2026-04-08 18:11:47 +02:00
darken 56e01cfde9 chore: Streamline PR description format in Claude rules 2026-04-08 18:07:52 +02:00
darken ed37e142ad fix(ui): Fix signal bars and restore individual connection icons 2026-04-08 18:07:06 +02:00
darken cb915d6263 feat(aap): Also update system Bluetooth alias when renaming
After the AAP rename succeeds, try to update Android's per-device bond alias via the hidden BluetoothDevice.setAlias(String) method, so the new name also shows up in the system Bluetooth settings on this phone.

Known failure mode on Android 12+: setAlias is gated behind a Companion Device Manager (CDM) association at the service layer, and raises 'does not have a CDM association with the Bluetooth Device' for third-party apps that don't hold one. In that case, a dedicated snackbar explains that the system rename didn't go through and suggests renaming manually in system settings or re-pairing.

The AAP-level rename is always attempted first and is the load-bearing part; the system alias is a best-effort extra.
2026-04-08 15:36:18 +02:00
darken 41245d928c fix(aap): Make AirPods rename actually apply on device
Switch the AAP rename packet to the opcode 0x1A format (04 00 04 00 1A 00 01 [size] 00 [name]) matching the LibrePods documentation and Linux implementation. The previous 0x1E variant (from the LibrePods Android code) was silently ignored by AirPods Pro 2 USB-C firmware — no 0x001D echo, no persistence across reconnect.

Verified on AirPods Pro 2 USB-C (firmware 81.2675...): the device now echoes the new name back via the next 0x001D INFORMATION message, and the name persists after disconnect/reconnect.

Also hardens the rename UX: gate the edit icon on isAapReady (was isAapConnected, which allowed sending during HANDSHAKING), apply an optimistic deviceInfo update with a scoped rollback on send failure, surface send errors via a new Event.SendFailed + snackbar, and restrict dialog input to ASCII with inline error feedback. Unifies send() / sendProGated() through a single sendInternal() helper so error plumbing benefits every command, not just rename.

Note: this only updates the AirPods firmware's self-reported name. Android's system Bluetooth settings read from the bond database and are not affected — renaming there still requires the Android system Bluetooth UI.
2026-04-08 15:36:18 +02:00
darken ccd95a0981 feat(aap): Show 'Not connected' card when AAP is unavailable 2026-04-08 11:49:07 +02:00
darken 2d38db5cba feat(aap): Log decoded 0x1D segments for engraving discovery
Diagnostic-only NUL-delimited UTF-8 segmentation of the 0x1D INFORMATION packet, logged at INFO so it lands in debug recordings via the existing FileLogger pipeline. The production decode path stays untouched — this only adds visibility, no behavior change. Refs #173.
2026-04-08 11:21:15 +02:00
darken 175f8e36b5 fix(aap): Address device settings review feedback
- Show connected device MACs in correct byte order (remove stale reversal)

- Rename 'Volume Swipe Length' setting to 'Volume Swipe Wait Time'

- Hide 'Charging Sounds' toggle: real case tones go over ATT and the actual effect of this AAP setting is unknown. Decode kept internally so AAP freshness signal still refreshes.

- Fix duplicate commands on End Call/Mute Mic radio buttons: use Modifier.selectable with Role.RadioButton, short-circuit when already selected, and wrap options in selectableGroup for TalkBack.
2026-04-07 18:53:54 +02:00
darken a102b03261 fix: Show signal strength as bar indicator instead of percent 2026-04-07 17:56:29 +02:00
darken 57fc4d8e7b ui: Remove noise control label from DeviceSettingsScreen 2026-04-07 15:36:20 +02:00
darken f67336d4fe fix(aap): Synthesize AAP-only devices for cold-start window 2026-04-07 15:24:03 +02:00
darken a511c57b7d fix(aap): Keep device settings visible when BLE scan goes stale 2026-04-07 13:28:37 +02:00
darken 66e3ab1670 Release: 5.0.0-beta0 2026-04-04 11:47:09 +02:00
darken 7e73ac80e9 fix(aap): Fix stem actions volume control, reset button, and Default/No Action semantics
Fix volume up/down doing nothing by using adjustSuggestedStreamVolume instead of dispatchMediaKeyEvent which ignores volume keycodes.

Add reset-to-defaults button with confirmation dialog in the stem actions TopAppBar.

Rename 'None' to 'Default' (firmware handles the press) and add 'No Action' (claimed but do nothing) to correctly model per-press-type claim mask semantics.

Remove disableNone lock; add cross-side auto-set logic so selecting Default resets both sides and selecting an action promotes the other side from Default to No Action.
2026-04-04 11:41:26 +02:00
darken 1ae9ae587e feat: Add device settings and controls to upgrade screen benefits 2026-04-04 11:41:26 +02:00
darken 4631aa8bb2 fix: Hide stale cached battery indicator when AAP is connected 2026-04-04 11:41:26 +02:00
darken 33c8fe76a6 fix(aap): Polish noise control UI and fix ANC mode switching
Combine ANC mode selector with listening mode cycle into a card with eye icon visibility toggles and radio button mode selection. Send AllowOff command when toggling OFF visibility.

Fix ANC resend logic fighting rapid mode changes by cancelling pending resend jobs on new commands. Reorder settings into Noise Control, Sound, Controls, General, and Connections sections.
2026-04-04 11:41:26 +02:00
darken e356344ba6 feat(aap): Add new device settings, stem actions, and noise control UI
Add 8 new AAP writable settings (microphone mode, ear detection toggle, listening mode cycle, allow off, stem config, sleep detection, in-case tone, device rename) and 4 new data reception features (stem press events, connected devices, audio source, EQ data).

Implement stem press action system with per-bud configurable Android actions (play/pause, next/prev track, volume), auto-sent stem config on connection, and dedicated config screen.

Combine ANC mode selector with listening mode cycle visibility into a unified noise control component. Eye icons control which modes appear in the dashboard card and settings. Pro gating with stars icon for upgrade-required features.

Add 64 unit tests covering all new decoders, model feature flags, malformed payloads, and rename byte-length validation.
2026-04-04 11:41:26 +02:00
darken e014ad8045 feat: Slim down overview cards and fix cached device icons
Remove timestamps, connection state, and conversation awareness from overview cards. Move signal badge inline with model subtitle. Relocate removed info to device settings screen info card. Fix cached devices showing Gen 1 icons by adding per-pod icon properties to PodModel. Add profile name subtitle to device settings toolbar. Simplify preview coverage to full/minimal/cached variants.
2026-04-03 15:36:27 +02:00
darken 9d446938bd fix(aap): Re-send ANC mode when firmware overrides after ear insertion 2026-04-03 13:38:53 +02:00
darken fea1092a95 refactor: Move AAP auto-connect from MonitorService to DeviceMonitor scope
AAP connections now run in appScope via AapLifecycleManager, independent of MonitorMode. Fixes AAP not connecting when monitor mode is MANUAL.

Also fixes reconnect cleanup on flow cancellation (try/finally).
2026-04-03 13:27:52 +02:00
darken adfedc1cce feat: Add device settings screen for AAP controls 2026-04-03 13:09:31 +02:00
darken c609b03148 feat(overview): Gate dashboard device count for free users
Free users see only their highest-priority profiled device. Additional devices are hidden behind an upgrade card showing the count and a flavor-aware CTA (Upgrade/Donate).
2026-04-02 18:30:40 +02:00
Matthias Urhahn 927a198d8e Merge pull request #475 from d4rken-org/feat/profile-drag-reorder
Device: Add drag-to-reorder for device profiles
2026-04-02 18:18:08 +02:00
darken 1f5f406b33 feat(profiles): Add drag-to-reorder for profile list
Wire up the missing drag-to-reorder UI that the priority hint already advertises. Long-press a profile row to reorder. Extracts a reusable ReorderableState component into common/compose.

Backend (repo + ViewModel) was already in place; this adds the gesture handling, auto-scroll, visual feedback, and ID-based reorder validation.
2026-04-02 18:17:00 +02:00
Matthias Urhahn 4a475419e8 Merge pull request #469 from d4rken-org/feat/device-state-cache
Device: Remember battery levels and show offline device cards
2026-04-02 17:36:59 +02:00
darken 7c37854076 refactor: Extract toCachedState extension and add tests
Move per-device persist logic from DeviceMonitor into a pure PodDevice.toCachedState() extension in the cache package. Add ToCachedStateTest with coverage for creates, skips, dedup, and slot preservation. Delete unused PodSorter.
2026-04-02 16:46:56 +02:00
darken 30d7efeec2 Moving packages around 2026-04-02 16:40:56 +02:00
darken 942e03af36 refactor: Merge device state persistence into DeviceMonitor
Eliminate DeviceStatePersister class by chaining persistence as a side effect in DeviceMonitor's flow. The flow is now shared via replayingShare(appScope) so persistence runs once per emission regardless of subscriber count, and works for BLE-only devices without MonitorService.
2026-04-02 16:36:12 +02:00
darken 24a84a49f1 refactor: Remove meta from PodDevice public API and delete unused PodMonitorExtensions
All external consumers now use top-level PodDevice properties. BlePodMonitor extensions were dead code — all callers use DeviceMonitor equivalents.
2026-04-02 16:01:00 +02:00
darken e3693628c2 refactor: Use PodDevice properties instead of reaching through BLE metadata
Replace device.meta?.profile?.address/id/label with device.address/profileId/label in AutoConnect, PopUpReaction, and BatteryGlanceWidget.
2026-04-02 15:42:12 +02:00
darken 94156b6200 fix: Show profile name on cached-only device cards
Add label property to PodDevice, populated from the profile in DeviceMonitor. Cards now use device.label instead of reaching through BLE metadata, so cached-only cards display the profile name instead of '?'.
2026-04-02 15:37:33 +02:00
darken 5ae15d828d refactor: Use PodDevice charging properties instead of duplicating fallback logic 2026-04-02 15:23:16 +02:00
darken 360067b07e refactor: Move persisters to monitor package and fix stale BLE device eviction
Move DeviceStatePersister and AapKeyPersister from reaction to monitor package since they are always-on infrastructure, not user-togglable reactions.

Add periodic ticker to BlePodMonitor to force stale device eviction when BLE scanner produces no results, fixing cached card not appearing after disconnect.
2026-04-02 15:14:47 +02:00
darken e91243e577 feat: Add unified device state cache for persistent battery display
Replace PodDeviceCache (raw BLE scan bytes) with DeviceStateCache that stores decoded combined device state (battery, charging, model) per profile.

Battery values persist across app restarts with per-slot timestamps. Cached-only cards appear for offline devices with muted visuals and a staleness indicator. Fallback chain: AAP -> BLE -> cached.
2026-04-02 13:56:09 +02:00
darken 19d61ca5cb feat: Add AAP session tests for AirPods Pro 2 USB-C
Add AirPodsPro2UsbcAapSessionTest with real captured bytes from a live

device session (model A3048, Pixel 8, 2026-04-02). Covers handshake,

device info, battery states, private keys, all Pro 2 USB-C settings

(including ADAPTIVE ANC, VolumeSwipe, ConversationalAwareness),

ear detection across 6 transitions, and unhandled messages.

Also document ChargingState observations across models and

EndCallMuteMic subtype variations in code comments.
2026-04-02 13:45:52 +02:00
darken 7b403defab fix: Add Compose stability annotations and fix lazy list keys
Add @Stable to PodDevice to enable Compose referential equality checks, reducing unnecessary recompositions from the 3-second update ticker. Make icon properties non-null with built-in defaults and fix lazy list keys to use stable string identifiers.
2026-04-02 13:18:05 +02:00
darken ceb4108306 feat(profiles): Filter paired devices already used by other profiles
Show devices claimed by other profiles as disabled in the dropdown with the claiming profile's name. Enforce address uniqueness at the repo layer.
2026-04-02 12:23:06 +02:00
darken fe4ac306cf fix: Correct feature flags for multiple PodModel entries
Remove false positive hasEarDetection from Beats Studio Buds and Studio Buds+ (neither has in-ear detection). Remove false positive hasVolumeSwipe and hasVolumeSwipeLength from AirPods 4 ANC (volume swipe is exclusive to AirPods Pro). Add missing hasEarDetection to AirPods Max, AirPods Max USB-C, AirPods Gen 4, Beats Solo Pro, and Beats Studio 3 (all have head/ear detection per Apple docs).
2026-04-01 06:57:45 +02:00
darken 618229b96b feat: Add AAP session tests for AirPods Pro Gen 1
Add AirPodsProAapSessionTest with real captured protocol data from AirPods Pro (A2084). Fix AIRPODS_PRO feature flags: remove hasVolumeSwipe/Length (hardware limitation), add hasEndCallMuteMic (firmware-supported). Fix primary pod decoder to accept byte[2]=0x00 format sent by Pro 1 on initial connect.
2026-03-31 22:36:23 +02:00
darken 1931e60b07 feat: Speed up AAP L2CAP reconnection
Only attempt AAP connections to classically-connected devices, eliminating futile retries for devices connected to other phones.

- Filter initialConnect() to devices in connectedAddresses

- Use mapLatest so stale retry loops cancel on state changes

- Parallelize per-profile connection attempts

- Add 5s connect timeout (best-effort) to cap retry cycles

- Add classic BT check to reconnectOnDisconnect()

- Keep MonitorService alive when AAP connections are active
2026-03-31 22:06:23 +02:00
darken 7bdc9ca212 feat: Auto-correct device model from AAP device info
When connected via AAP, the device reports its hardware model number. If the profile has a wrong or missing model, detect the mismatch and automatically correct it with a reconnect to apply correct feature flags (ANC modes, InitExt, etc.).

Adds modelNumbers field to PodModel enum with Apple hardware identifiers for all known devices, and a fromModelNumber() lookup function.
2026-03-31 20:52:57 +02:00
darken 04ea3d0cd8 feat: Add retry logic for AAP L2CAP connection failures
Initial connect had no retry — a single failed L2CAP attempt was silently swallowed. Reconnect-on-disconnect used separate longer backoff delays.

Both paths now share the same retry schedule (3s,3s,3s,5s,5s,10s,10s) giving 7 retries over ~39s. Initial connect checks if another path already reconnected before each retry.
2026-03-31 19:17:09 +02:00
darken fef6c97d2d feat: Detect primary pod via AAP cmd 0x0008 for faster ear detection 2026-03-31 19:17:09 +02:00
darken dc2e318374 fix: Trigger AAP connection when classic Bluetooth connects
initialConnect() only ran on profile changes, so if the L2CAP connection failed at service start (device not yet connected), it was never retried. Now also triggers on connectedDevices changes.

Also fix reconnect BLE address comparison: was comparing BLE RPA with bonded BR/EDR address (never matches), now uses profile address.
2026-03-31 19:17:09 +02:00
darken 71638c13ab feat: Decode AAP ear detection and stabilize ANC mode changes
Decode cmd 0x0006 as EarDetection with per-pod placement (IN_EAR, NOT_IN_EAR, IN_CASE, DISCONNECTED). Map AAP primary/secondary to left/right using BLE primary pod bit.

Queue ANC mode changes when no pod is in ear, auto-send when a pod goes in ear. Show pending mode in UI with secondary color treatment.

Debounce device-initiated ANC mode cycling during ear transitions. Skip debounce for user-initiated commands and initial handshake. Optimistic UI update on send for instant feedback.
2026-03-31 19:17:09 +02:00
darken bd5c6bf858 feat: Add MagicPodsCore and LibrePods to acknowledgements 2026-03-31 19:17:09 +02:00
darken 70e5b99e1d feat: Add connection type indicators to SignalBadge
Move BLE key and AAP connection icons into the SignalBadge pill.

Add BleKeyState enum on PodDevice to expose IRK/ENC state cleanly.

Key icon: outlined for IRK-only, solid for IRK+ENC.

Bluetooth icon shown when AAP transport is active.
2026-03-31 19:17:09 +02:00
darken f65c6e4a29 feat: Add BLE device support for 6 new Beats models
Solo Pro (0x0C20), Solo 4 (0x2520), Solo Buds (0x2620), Studio Buds (0x1120), Studio Buds+ (0x1620), Studio Pro (0x1720). Tests use handcrafted data pending real captures.
2026-03-31 19:17:09 +02:00
darken c860ecc430 feat: Integrate AAP connection freshness into signal quality indicator 2026-03-31 19:17:09 +02:00
darken a8593eecd3 refactor: Reorganize pods package into ble/ and aap/ domains
Move BLE-specific code (snapshots, devices, factories, protocol) under apple/ble/. Move AAP code under apple/aap/ with protocol/ subdirectory. PodModel stays in apple/ as the only shared type.
2026-03-31 19:17:09 +02:00
darken 94e28fb0b5 refactor: Move Proximity* classes into protocol.ble package 2026-03-31 19:17:09 +02:00
darken 4808e04146 fix: Update Dagger component reference after BaseBlePodsTest rename 2026-03-31 19:17:09 +02:00
darken aebf43c355 style: Reformat PodModel enum for better maintainability
Standardize the formatting of pod models and their feature sets. This improves the readability of device-specific capabilities and ensures cleaner diffs for future hardware additions.
2026-03-31 19:17:09 +02:00
darken c1cd99f701 feat: Add AAP L2CAP protocol support for reading and writing AirPods settings
Full integration of the Apple Accessory Protocol (AAP) over L2CAP, enabling direct communication with AirPods for 1% battery granularity, ANC mode control, Conversation Awareness toggle, and private key exchange for BLE encrypted battery.
2026-03-31 19:17:09 +02:00
darken 3fa01bb510 refactor: Rename PodDevice to BlePodSnapshot, MonitoredDevice to PodDevice, extract PodModel
Complete type rename chain: PodDevice (interface) becomes BlePodSnapshot, MonitoredDevice (facade) claims PodDevice name, Model enum extracted to top-level PodModel. Delete redundant type alias files. Fix stale comments and log tags.
2026-03-31 19:17:09 +02:00
darken e84f8762b4 Extract Model to PodModel.kt 2026-03-31 19:17:09 +02:00
darken ac21261d4d refactor: Migrate all consumers from PodDevice to MonitoredDevice
Replace direct PodDevice/PodMonitor usage with MonitoredDevice/DeviceMonitor across ViewModels, UI screens, notifications, widgets, reactions, and service. All interface cast-based property access replaced with flat MonitoredDevice properties. Make L2capSocketFactory injectable.
2026-03-31 19:17:09 +02:00
darken c19ee88ae6 refactor: Add BlePodSnapshot and BlePodMonitor type aliases
Introduce type aliases for the planned rename (PodDevice -> BlePodSnapshot, PodMonitor -> BlePodMonitor). New code uses the aliases to clarify BLE-specific types vs the unified MonitoredDevice facade.

Full codebase rename deferred to IDE refactoring pass (103 files, 470 occurrences).
2026-03-31 19:17:09 +02:00
darken f751cdb37e feat: Add MonitoredDevice facade and DeviceMonitor merge point
MonitoredDevice unifies BLE and AAP data sources with dynamic resolution.

DeviceMonitor combines PodMonitor + AapConnectionManager into a single Flow.

To be renamed to PodDevice/PodMonitor when the old types are renamed to BlePodSnapshot/BlePodMonitor.
2026-03-31 19:17:09 +02:00
darken 28cd3b1340 test: Add unit tests for AAP protocol layer and Model.Features 2026-03-31 19:17:09 +02:00
darken 3e2852e622 feat: Add AAP protocol layer, L2CAP socket factory, and Model.Features
Add L2CAP infrastructure (HiddenApiBypass, ArtMirror, L2capSocketFactory) in common/bluetooth/l2cap/ for BR/EDR L2CAP socket creation on Android 17+.

Add AAP protocol abstraction (AapSetting, AapCommand, AapMessage, AapFramer, AapDeviceProfile, AapConnection, AapConnectionManager) with sealed domain classes containing zero wire bytes.

Add Model.Features capability flags (hasDualPods, hasCase, hasEarDetection, hasAncControl) to all 25 device model enum entries.

Add debug-only L2capPocActivity for testing AAP connections.
2026-03-31 19:17:09 +02:00
darken b919d8d19b chore: Harden GitHub Actions workflow permissions
Add explicit permissions and persist-credentials: false to all workflows.

Without an explicit permissions block, GITHUB_TOKEN inherits the repo default (write-all). These CI workflows only need contents: read. The release workflow already declares contents: write at job level where needed.

persist-credentials: false prevents the token from lingering in .git/config for subsequent steps, reducing attack surface if a third-party action is compromised.
2026-03-28 09:22:01 +01:00
577 changed files with 40300 additions and 4112 deletions
+2 -1
View File
@@ -1 +1,2 @@
worktrees/
worktrees/
scheduled_tasks.lock
+23 -2
View File
@@ -40,6 +40,18 @@ PR titles appear in auto-generated changelogs and are read by users. Use **ELI5,
## PR Description Format
PRs are reviewed in **GitHub's web UI**, which already shows the file tree, the diff, and the tests. Don't duplicate any
of it. The description should answer questions the diff can't — not restate it.
Only these sections, in this order:
1. `## What changed`
2. `## Technical Context`
3. `## Review checklist` *(optional)*
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sub-sections — GitHub shows the files and tests, and review
notes belong in the checklist. Fold anything critical into a Technical Context bullet.
### What changed
User-friendly explanation of what this PR does. Describe the problem that was fixed or the feature that was added from the user's perspective. No internal class or method names.
@@ -53,9 +65,18 @@ Explain what's hard to extract from the diff alone. Focus on:
- **Why** this approach was chosen (and alternatives considered/rejected)
- **Root cause** for bug fixes (the diff shows the fix, not what caused it)
- **Non-obvious side effects** or behavioral changes not apparent from reading the code
- **Review guidance** — what's tricky or deserves close attention
Keep it scannable with bullet points. Don't restate what's visible in the diff (file names, class renames, line-level changes).
Format rules:
- **One bullet per point.** No prose paragraphs, no nested sub-headers like `**Bug 1** / **Bug 2**` — if a PR fixes
multiple bugs, one bullet per bug is enough.
- **Don't restate the diff.** File paths, class renames, test names, and line-level changes are all visible in the web
UI.
### Review checklist (optional)
For PRs with multiple non-trivial review points, add a `## Review checklist` section with `- [ ]` tasks the reviewer can
tick off as they verify. Skip it for small PRs — a single tricky thing can stay as a Technical Context bullet.
### Example
+11
View File
@@ -6,6 +6,9 @@ on:
pull_request:
branches: [ main ]
permissions:
contents: read
jobs:
lint-vital:
name: Lint vitals
@@ -19,6 +22,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Setup project and build environment
uses: ./.github/actions/common-setup
@@ -37,6 +42,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Setup project and build environment
uses: ./.github/actions/common-setup
@@ -54,6 +61,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Setup project and build environment
uses: ./.github/actions/common-setup
@@ -66,5 +75,7 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Validate metadata lengths
run: bash fastlane/check_metadata_length.sh
@@ -8,10 +8,15 @@ on:
branches:
- main
permissions:
contents: read
jobs:
validation:
name: "Validation"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c #v5.0.2
+5
View File
@@ -11,6 +11,9 @@ on:
type: boolean
default: true
permissions:
contents: read
jobs:
release-github:
name: Create GitHub release
@@ -33,6 +36,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup project and build environment
uses: ./.github/actions/common-setup
@@ -108,6 +112,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup project and build environment
uses: ./.github/actions/common-setup
+3 -1
View File
@@ -16,4 +16,6 @@
/fastlane/README.md
.kotlin
# Screenshot test reference images (ephemeral, regenerated on demand)
app/src/screenshotTest*/reference/
app/src/screenshotTest*/reference/
.codex
protocol-research/
+11 -1
View File
@@ -1,4 +1,4 @@
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/banner.png" width="400">
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/banner.png" width="400" alt="CAPod banner">
# Companion App for AirPods (CAPod)
@@ -34,11 +34,20 @@ Currently supported models:
* AirPods Pro 2. Generation (USB-C)
* AirPods Pro 3. Generation
* AirPods Max
* AirPods Max (USB-C)
* AirPods Max 2. Generation
* Power Beats Pro
* Power Beats Pro 2
* Power Beats 3
* Power Beats 4
* Beats Solo 3
* Beats Solo Pro
* Beats Solo 4
* Beats Solo Buds
* Beats Studio 3
* Beats Studio Buds
* Beats Studio Buds+
* Beats Studio Pro
* Beats X
* Beats Flex
* Beats Fit Pro
@@ -87,6 +96,7 @@ Currently supported models:
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.
* [Crowdin](https://crowdin.com/) for supporting open-source projects.
## License
+1 -1
View File
@@ -1 +1 @@
4.0.3-rc0 40003000
5.1.0-rc0 50100000
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.darken.capod.debug.l2cap.L2capPocActivity"
android:exported="true"
android:label="L2CAP PoC">
<intent-filter>
<action android:name="eu.darken.capod.L2CAP_POC" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,256 @@
package eu.darken.capod.debug.l2cap
import eu.darken.capod.common.bluetooth.l2cap.L2capSocketFactory
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class L2capPocActivity : ComponentActivity() {
companion object {
private const val TAG = "L2capPoc"
private const val PSM = 0x1001
private val HANDSHAKE = byteArrayOf(
0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
private val ANC_OFF = byteArrayOf(
0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x00
)
private val ANC_ON = byteArrayOf(
0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x0d, 0x02, 0x00, 0x00, 0x00
)
private val TRANSPARENCY = byteArrayOf(
0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x00
)
}
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) Log.d(TAG, "BLUETOOTH_CONNECT granted")
else Log.w(TAG, "BLUETOOTH_CONNECT denied")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
) {
permissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
PocScreen()
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@SuppressLint("MissingPermission")
@Composable
private fun PocScreen() {
val btManager = remember { getSystemService(BluetoothManager::class.java) }
val adapter: BluetoothAdapter? = remember { btManager?.adapter }
val logEntries = remember { mutableStateListOf<String>() }
val logListState = rememberLazyListState()
var selectedDevice by remember { mutableStateOf<BluetoothDevice?>(null) }
var socket by remember { mutableStateOf<BluetoothSocket?>(null) }
var outputStream by remember { mutableStateOf<OutputStream?>(null) }
var connected by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var readerJob by remember { mutableStateOf<Job?>(null) }
val timeFmt = remember { SimpleDateFormat("HH:mm:ss.SSS", Locale.US) }
fun log(msg: String) {
val line = "${timeFmt.format(Date())} $msg"
Log.d(TAG, msg)
logEntries.add(line)
}
fun ByteArray.hex(): String = joinToString(" ") { "%02X".format(it) }
LaunchedEffect(logEntries.size) {
if (logEntries.isNotEmpty()) logListState.animateScrollToItem(logEntries.size - 1)
}
fun startReader(input: InputStream) {
readerJob = scope.launch(Dispatchers.IO) {
val buf = ByteArray(1024)
try {
while (isActive) {
val len = input.read(buf)
if (len == -1) {
launch(Dispatchers.Main) { log("<<< Stream closed by remote") }
break
}
val data = buf.copyOf(len)
launch(Dispatchers.Main) { log("<<< [${data.size}] ${data.hex()}") }
}
} catch (e: Exception) {
launch(Dispatchers.Main) { log("<<< Read error: ${e.message}") }
}
}
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("L2CAP PoC — AAP Connection", style = MaterialTheme.typography.titleMedium)
if (adapter == null) {
Text("No Bluetooth adapter found", color = MaterialTheme.colorScheme.error)
return@Column
}
// Bonded devices
if (!connected) {
Text("Bonded Devices:", style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(top = 8.dp))
val bonded = remember(adapter) {
try { adapter.bondedDevices?.toList() ?: emptyList() }
catch (_: SecurityException) { emptyList() }
}
bonded.forEach { device ->
val name = try { device.name ?: "Unknown" } catch (_: SecurityException) { "Unknown" }
val isSelected = selectedDevice == device
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp).clickable { selectedDevice = device },
colors = if (isSelected) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) else CardDefaults.cardColors()
) {
Row(modifier = Modifier.padding(8.dp)) {
Text("$name ${device.address}", style = MaterialTheme.typography.bodySmall)
}
}
}
}
// Connection controls
FlowRow(modifier = Modifier.padding(vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {
val dev = selectedDevice ?: return@Button
log("=== Connecting to ${dev.address} ===")
scope.launch(Dispatchers.IO) {
try {
val sock = L2capSocketFactory().createSocket(dev, PSM)
withContext(Dispatchers.Main) { log("Socket created, connecting...") }
sock.connect()
withContext(Dispatchers.Main) {
log("Connected!")
socket = sock
outputStream = sock.outputStream
connected = true
}
startReader(sock.inputStream)
withContext(Dispatchers.Main) { log(">>> Handshake [${HANDSHAKE.size}] ${HANDSHAKE.hex()}") }
sock.outputStream.write(HANDSHAKE)
sock.outputStream.flush()
} catch (e: Exception) {
withContext(Dispatchers.Main) { log("Failed: ${e::class.simpleName}: ${e.message}") }
}
}
},
enabled = selectedDevice != null && !connected
) { Text("Connect") }
Button(
onClick = {
log("=== Disconnecting ===")
readerJob?.cancel()
readerJob = null
try { socket?.close() } catch (_: Exception) {}
socket = null
outputStream = null
connected = false
},
enabled = connected
) { Text("Disconnect") }
}
// AAP command buttons
if (connected) {
FlowRow(modifier = Modifier.padding(vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
fun sendCommand(name: String, cmd: ByteArray) {
scope.launch(Dispatchers.IO) {
try {
withContext(Dispatchers.Main) { log(">>> $name [${cmd.size}] ${cmd.hex()}") }
outputStream?.write(cmd)
outputStream?.flush()
} catch (e: Exception) {
withContext(Dispatchers.Main) { log(">>> Send error: ${e.message}") }
}
}
}
Button(onClick = { sendCommand("ANC Off", ANC_OFF) }) { Text("ANC Off") }
Button(onClick = { sendCommand("ANC On", ANC_ON) }) { Text("ANC On") }
Button(onClick = { sendCommand("Transparency", TRANSPARENCY) }) { Text("Transparency") }
Button(onClick = { sendCommand("Handshake", HANDSHAKE) }) { Text("Handshake") }
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
LazyColumn(state = logListState, modifier = Modifier.weight(1f)) {
items(logEntries) { entry ->
Text(text = entry, fontFamily = FontFamily.Monospace, fontSize = 11.sp, lineHeight = 14.sp)
}
}
}
}
}
@@ -11,32 +11,28 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.toArgb
import eu.darken.capod.R
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.main.ui.widget.ComposeWidgetPreview
import eu.darken.capod.main.ui.widget.WidgetRenderState
import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard
import eu.darken.capod.main.ui.widget.WidgetConfigurationScreen
import eu.darken.capod.main.ui.widget.WidgetConfigurationViewModel
import eu.darken.capod.main.ui.widget.WidgetTheme
import eu.darken.capod.common.compose.preview.MOCK_NOW
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.common.theming.CapodTheme
import eu.darken.capod.main.ui.overview.OverviewScreen
import eu.darken.capod.main.ui.overview.OverviewViewModel
import eu.darken.capod.main.ui.settings.SettingsScreen
import eu.darken.capod.main.ui.settings.SettingsViewModel
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.main.ui.widget.ComposeWidgetPreview
import eu.darken.capod.main.ui.widget.WidgetConfigurationScreen
import eu.darken.capod.main.ui.widget.WidgetConfigurationViewModel
import eu.darken.capod.main.ui.widget.WidgetRenderState
import eu.darken.capod.main.ui.widget.WidgetTheme
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.profiles.ui.DeviceManagerScreen
import eu.darken.capod.profiles.ui.DeviceManagerViewModel
import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationScreen
import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationViewModel
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
import eu.darken.capod.reaction.ui.ReactionSettingsScreen
import eu.darken.capod.reaction.ui.ReactionSettingsViewModel
import eu.darken.capod.main.ui.devicesettings.DeviceSettingsScreen
import eu.darken.capod.main.ui.devicesettings.DeviceSettingsViewModel
import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard
internal const val DS = "spec:width=1080px,height=2400px,dpi=428"
@@ -47,20 +43,21 @@ internal fun DashboardContent() = PreviewWrapper {
now = MOCK_NOW,
permissions = emptySet(),
devices = listOf(
MockPodDataProvider.airPodsProMixed(),
MockPodDataProvider.airPodsMax(),
MockPodDataProvider.unknownDevice(),
MockPodDataProvider.dualPodMonitoredMixed(),
MockPodDataProvider.singlePodMonitored(),
MockPodDataProvider.unknownMonitored(),
),
isDebugMode = false,
isBluetoothEnabled = true,
profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
MockPodDataProvider.profile("My AirPods Pro", PodModel.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodModel.AIRPODS_MAX),
),
upgradeInfo = MockPodDataProvider.gplayInfo(isPro = true),
showUnmatchedDevices = false,
),
onRequestPermission = {},
onBluetoothSettings = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
@@ -73,9 +70,9 @@ internal fun DeviceProfilesContent() = PreviewWrapper {
DeviceManagerScreen(
state = DeviceManagerViewModel.State(
profiles = listOf(
MockPodDataProvider.profile("Work AirPods", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
MockPodDataProvider.profile("Gym Beats", PodDevice.Model.POWERBEATS_PRO),
MockPodDataProvider.profile("Work AirPods", PodModel.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodModel.AIRPODS_MAX),
MockPodDataProvider.profile("Gym Beats", PodModel.POWERBEATS_PRO),
),
),
onBack = {},
@@ -92,11 +89,11 @@ internal fun AddProfileContent() = PreviewWrapper {
name = "",
nameError = null,
selectedModel = null,
availableModels = PodDevice.Model.entries.filter { it != PodDevice.Model.UNKNOWN },
availableModels = PodModel.entries.filter { it != PodModel.UNKNOWN },
identityKey = null,
encryptionKey = null,
selectedDevice = null,
bondedDevices = emptyList(),
bondedDeviceItems = emptyList(),
minimumSignalQuality = 0.15f,
canSave = false,
),
@@ -114,52 +111,23 @@ internal fun AddProfileContent() = PreviewWrapper {
}
@Composable
internal fun SettingsIndexContent() = PreviewWrapper {
SettingsScreen(
state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"),
onNavigateUp = {},
onGeneralSettings = {},
onDeviceManager = {},
onReactions = {},
onSupport = {},
onWiki = {},
onChangelog = {},
onHelpTranslate = {},
onAcknowledgements = {},
onPrivacyPolicy = {},
onSponsor = {},
)
}
@Composable
internal fun ReactionSettingsContent() = PreviewWrapper {
ReactionSettingsScreen(
state = ReactionSettingsViewModel.State(
internal fun DeviceSettingsReactionsContent() = PreviewWrapper {
DeviceSettingsScreen(
state = DeviceSettingsViewModel.State(
device = MockPodDataProvider.dualPodMonitoredWithReactions(),
now = MOCK_NOW,
isPro = true,
onePodMode = false,
autoPlay = true,
autoPause = true,
autoConnect = false,
autoConnectCondition = AutoConnectCondition.WHEN_SEEN,
showPopUpOnCaseOpen = true,
showPopUpOnConnection = false,
isNudgeAvailable = true,
),
onNavigateUp = {},
onOnePodModeChanged = {},
onAutoPlayChanged = {},
onAutoPauseChanged = {},
onAutoConnectChanged = {},
onAutoConnectConditionSelected = {},
onShowPopUpOnCaseOpenChanged = {},
onShowPopUpOnConnectionChanged = {},
)
}
@Composable
internal fun WidgetConfigurationContent() = PreviewWrapper {
val profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
MockPodDataProvider.profile("My AirPods Pro", PodModel.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodModel.AIRPODS_MAX),
)
WidgetConfigurationScreen(
state = WidgetConfigurationViewModel.State(
@@ -201,7 +169,7 @@ internal fun CasePopUpContent() {
contentAlignment = Alignment.Center,
) {
PopUpCard(
device = MockPodDataProvider.airPodsProMixed(),
device = MockPodDataProvider.dualPodMonitoredMixed(),
onClose = {},
modifier = Modifier.padding(horizontal = 32.dp),
)
@@ -261,14 +229,10 @@ private fun PreviewDeviceProfiles() = DeviceProfilesContent()
@Composable
private fun PreviewAddProfile() = AddProfileContent()
@Preview(name = "7 - Settings", locale = "en", device = DS, showSystemUi = true)
@Preview(name = "7 - Device Settings Reactions", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewSettingsIndex() = SettingsIndexContent()
private fun PreviewDeviceSettingsReactions() = DeviceSettingsReactionsContent()
@Preview(name = "8 - Reaction Settings", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewReactionSettings() = ReactionSettingsContent()
@Preview(name = "9 - Homescreen Widget", device = DS, showSystemUi = true)
@Preview(name = "8 - Homescreen Widget", device = DS, showSystemUi = true)
@Composable
private fun PreviewHomescreenWidget() = HomescreenWidgetContent()
@@ -5,14 +5,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -24,6 +21,8 @@ import androidx.compose.material.icons.twotone.BluetoothConnected
import androidx.compose.material.icons.twotone.Favorite
import androidx.compose.material.icons.twotone.Palette
import androidx.compose.material.icons.twotone.PlayCircle
import androidx.compose.material.icons.twotone.Headphones
import androidx.compose.material.icons.twotone.Tune
import androidx.compose.material.icons.twotone.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@@ -50,6 +49,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -108,9 +108,11 @@ fun UpgradeScreen(
val benefits = listOf(
Benefit(Icons.TwoTone.Palette, R.string.upgrade_benefit_themes),
Benefit(Icons.TwoTone.PlayCircle, R.string.upgrade_benefit_autoplay),
Benefit(Icons.TwoTone.BluetoothConnected, R.string.upgrade_benefit_autoconnect),
Benefit(Icons.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
Benefit(Icons.TwoTone.Tune, R.string.upgrade_benefit_device_settings),
Benefit(Icons.TwoTone.Headphones, R.string.upgrade_benefit_device_controls),
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
)
@@ -118,138 +120,144 @@ fun UpgradeScreen(
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.surface,
) { paddingValues ->
Box(modifier = Modifier
.windowInsetsPadding(WindowInsets.systemBars)
.padding(paddingValues)
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(16.dp))
Box(contentAlignment = Alignment.Center) {
Surface(
modifier = Modifier.size(120.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),
) {}
Image(
painter = painterResource(R.drawable.splash_graphic2),
contentDescription = null,
modifier = Modifier.size(80.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = buildAnnotatedString {
append("CAPod ")
withStyle(SpanStyle(color = colorResource(R.color.brand_secondary), fontWeight = FontWeight.Bold)) {
append("FOSS")
}
},
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(24.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
Box(modifier = Modifier.padding(paddingValues)) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(48.dp))
Box(contentAlignment = Alignment.Center) {
Surface(
modifier = Modifier.size(120.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),
) {}
Image(
painter = painterResource(R.drawable.splash_graphic2),
contentDescription = null,
modifier = Modifier.size(80.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.upgrade_foss_preamble),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
text = buildAnnotatedString {
append("CAPod ")
withStyle(SpanStyle(color = colorResource(R.color.brand_secondary), fontWeight = FontWeight.Bold)) {
append("FOSS")
}
},
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
benefits.forEach { benefit ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(28.dp),
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.upgrade_foss_preamble),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
benefits.forEach { benefit ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(16.dp),
)
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(28.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(16.dp),
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(benefit.textRes),
style = MaterialTheme.typography.bodyLarge,
)
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(benefit.textRes),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
Text(
text = stringResource(R.string.upgrade_benefit_disclaimer),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSponsor,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp),
) {
Icon(
imageVector = Icons.TwoTone.Favorite,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.upgrade_foss_sponsor_action),
style = MaterialTheme.typography.titleMedium,
)
}
Text(
text = stringResource(R.string.upgrade_foss_sponsor_subtitle),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
Spacer(modifier = Modifier.height(24.dp))
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSponsor,
IconButton(
onClick = onNavigateUp,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp),
.align(Alignment.TopStart)
.padding(4.dp),
) {
Icon(
imageVector = Icons.TwoTone.Favorite,
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.upgrade_foss_sponsor_action),
style = MaterialTheme.typography.titleMedium,
)
}
Text(
text = stringResource(R.string.upgrade_foss_sponsor_subtitle),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
Spacer(modifier = Modifier.height(24.dp))
}
IconButton(
onClick = onNavigateUp,
modifier = Modifier.padding(4.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Borg ontwikkeling</string>
<string name="upgrade_foss_sponsor_subtitle">Geen advertensies. Geen opsporing. Geen Google Play-binding.</string>
<string name="upgrade_foss_sponsor_returned_early">Alreeds terug? U ondersteuning hou CAPod aan die lewe.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ልማትን ይደግፉ</string>
<string name="upgrade_foss_sponsor_subtitle">ምንም ማስታወቂያ የሉም። ምንም ክትትል የሉም። ምንም Google Play ቆለፋ የሉም።</string>
<string name="upgrade_foss_sponsor_returned_early">ቀድሞ ተመለሱ፡ ድጋፏዎ CAPodን ህያው ያቆይላል።</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ادعم التطوير</string>
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبع. لا قيود على Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">عدت بسرعة؟ دعمك يبقي CAPod حياً.</string>
<string name="upgrade_badge_label">البرمجيات الحرة (FOSS)</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">İnkişafı sponsor edin</string>
<string name="upgrade_foss_sponsor_subtitle">Reklam yoxdur. İzləmə yoxdur. Google Play bağlılığı yoxdur.</string>
<string name="upgrade_foss_sponsor_returned_early">Artıq geri qayıtdınız? Dəstəyiniz CAPod-u yaşadır.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Падтрымаць распрацоўку</string>
<string name="upgrade_foss_sponsor_subtitle">Ніякай рэкламы. Ніякай адсочкі. Ніякай залежнасці ад Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ужо вяртаецеся? Ваша падтрымка трымае CAPod ў жывых.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Спонсорирайте разработката</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклами. Без следене. Без заключване в Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Вече ли? Вашата подкрепа поддържа CAPod жив.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">স্পনসর ডেভলপমেন্ট</string>
<string name="upgrade_foss_sponsor_subtitle">কোনো বিজ্ঞাপন নেই। কোনো ট্র্যাকিং নেই। Google Play-এর সাথে বাঁধানো নেই।</string>
<string name="upgrade_foss_sponsor_returned_early">ইতোমধ্যে ফিরে গেলেন? আপনার সমর্থন CAPodকে বাঁচিয়ে রাখে।</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Patrocina el desenvolupament</string>
<string name="upgrade_foss_sponsor_subtitle">Sense anuncis. Sense seguiment. Sense dependència del Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ja heu tornat? El vostre suport manté viu el CAPod.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponzorovat vývoj</string>
<string name="upgrade_foss_sponsor_subtitle">Žádné reklamy. Žádné sledování. Žádná závislost na Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Už jste zpět? Vaše podpora udržuje CAPod naživu.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsorudvikling</string>
<string name="upgrade_foss_sponsor_subtitle">Ingen annoncer. Ingen sporing. Ingen Google Play-lås.</string>
<string name="upgrade_foss_sponsor_returned_early">Allerede tilbage? Din støtte holder CAPod i live.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Fördere Entwicklung</string>
<string name="upgrade_foss_sponsor_subtitle">Keine Werbung. Kein Tracking. Keine Google-Play-Bindung.</string>
<string name="upgrade_foss_sponsor_returned_early">Schon zurück? Deine Unterstützung hält CAPod am Leben.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Χορηγήστε την ανάπτυξη</string>
<string name="upgrade_foss_sponsor_subtitle">Χωρίς διαφημίσεις. Χωρίς παρακολούθηση. Χωρίς δέσμευση στο Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Επιστρέψατε ήδη; Η υποστήριξή σας κρατά το CAPod ζωντανό.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsorear el desarrollo</string>
<string name="upgrade_foss_sponsor_subtitle">Sin publicidad. Sin rastreo. Sin dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">¿Ya te vas? Tu apoyo mantiene CAPod vivo.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Patrocinar el desarrollo</string>
<string name="upgrade_foss_sponsor_subtitle">Sin anuncios. Sin rastreo. Sin dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">¿Ya de vuelta? Tu apoyo mantiene CAPod con vida.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Patrocinadores</string>
<string name="upgrade_foss_sponsor_subtitle">Sin anuncios. Sin rastreo. Sin dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">¿Ya de vuelta? Tu apoyo mantiene CAPod vivo.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Rahasta arendamist</string>
<string name="upgrade_foss_sponsor_subtitle">Reklaami pole. Jälgimist pole. Google Play lukustust pole.</string>
<string name="upgrade_foss_sponsor_returned_early">Juba tagasi? Sinu toetus hoiab CAPodi elus.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Garapena babestu</string>
<string name="upgrade_foss_sponsor_subtitle">Iragarkirik ez. Jarraipenik ez. Google Play-ren lotura ez.</string>
<string name="upgrade_foss_sponsor_returned_early">Dagoeneko itzuli? Zure laguntzak CAPod bizirik mantentzen du.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">حامی توسعه دهنده</string>
<string name="upgrade_foss_sponsor_subtitle">بدون تبلیغ. بدون ردیابی. بدون وابستگی به Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">زود برگشتیدی؟ حمایت شما CAPod را زنده نگه می‌دارد.</string>
<string name="upgrade_badge_label">منبع‌آزاد</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsoroi kehitystä</string>
<string name="upgrade_foss_sponsor_subtitle">Ei mainoksia. Ei seurantaa. Ei Google Play -riippuvuutta.</string>
<string name="upgrade_foss_sponsor_returned_early">Jo takaisin? Tukesi pitää CAPod-sovelluksen hengissä.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Mag-sponsor ng development</string>
<string name="upgrade_foss_sponsor_subtitle">Walang ads. Walang tracking. Walang Google Play lock-in.</string>
<string name="upgrade_foss_sponsor_returned_early">Bumalik na? Ang iyong suporta ay nagpapanatiling buhay ng CAPod.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Soutenir le développement</string>
<string name="upgrade_foss_sponsor_subtitle">Pas de publicités. Pas de suivi à la trace. Pas de dépendance à Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Déjà de retour? Votre soutien maintient CAPod en vie.</string>
<string name="upgrade_badge_label">Logiciel libre</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Patrocinar desenvolvemento</string>
<string name="upgrade_foss_sponsor_subtitle">Sen anuncios. Sen rastrexo. Sen dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">És rápido/a! O teu apoio mantén CAPod vivo.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">विकास को प्रायोजक करें</string>
<string name="upgrade_foss_sponsor_subtitle">कोई विज्ञापन नहीं। कोई ट्रैकिंग नहीं। Google Play पर निर्भरता नहीं।</string>
<string name="upgrade_foss_sponsor_returned_early">अभी वापस? आपका समर्थन CAPod को जीवित रखता है।</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Podržite razvoj</string>
<string name="upgrade_foss_sponsor_subtitle">Bez oglasa. Bez praćenja. Bez zaključanosti na Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Već se vraćate? Vaša podrška čuva CAPod na životu.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Szponzori fejlesztés</string>
<string name="upgrade_foss_sponsor_subtitle">Nincs hirdetés. Nincs nyomkövetés. Nincs Google Play-függőség.</string>
<string name="upgrade_foss_sponsor_returned_early">Már visszamész? A támogatásodnak köszönhetően él tovább a CAPod.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Հովանավորել զարգացումը</string>
<string name="upgrade_foss_sponsor_subtitle">Լրատնություն չկա: հետքխկություն չկա: Google Play-ի կապվածություն չկա:</string>
<string name="upgrade_foss_sponsor_returned_early">Արդևն հետ? ձևր աիկտակցությունը CAPod-ին կենդանի պահում:</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Pengembangan sponsor</string>
<string name="upgrade_foss_sponsor_subtitle">Tanpa iklan. Tanpa pelacakan. Tanpa ketergantungan Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Sudah kembali? Dukungan Anda menjaga CAPod tetap hidup.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Styrkja þróun</string>
<string name="upgrade_foss_sponsor_subtitle">Engar auglysíngar. Engin rakning. Ekkert Google Play-lás.</string>
<string name="upgrade_foss_sponsor_returned_early">Aftur þegar? Styðjað þít heldur CAPod lifandi.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Finanzia lo sviluppo</string>
<string name="upgrade_foss_sponsor_subtitle">Nessuna pubblicità. Nessun tracciamento. Nessun vincolo con Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Già via? Il tuo supporto mantiene CAPod in vita.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">פיתוח חסות</string>
<string name="upgrade_foss_sponsor_subtitle">אין פרסומות. אין מעקב. אין תלות ב-Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">כבר חוזר? התמיכה שלך שומרת את CAPod בחיים.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">開発支援者</string>
<string name="upgrade_foss_sponsor_subtitle">広告なし。追跡なし。Google Play依存なし。</string>
<string name="upgrade_foss_sponsor_returned_early">もう戻りますか?あなたのサポートがCAPodを支えています。</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">სპონსორის განვითარება</string>
<string name="upgrade_foss_sponsor_subtitle">რეკლამა არა. თვალთვალება არა. Google Play-ზე დამოკიდება არა.</string>
<string name="upgrade_foss_sponsor_returned_early">უკვე დაბრუნდით? თქვენი მხარდაჯერა CAPod-ს გადარჩენებას ეხმარება.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ឧបត្ថម្ភការអភិវឌ្ឍន៍</string>
<string name="upgrade_foss_sponsor_subtitle">គ្មានការផ្សាយពាណិជ្ជកម្ម។ គ្មានការតាមដាន។ គ្មានការចាក់សោ Google Play។</string>
<string name="upgrade_foss_sponsor_returned_early">ត្រលប់មកវិញហើយ។? ការគាំទ្ររបស់អ្នកចិញ្ចឹម CAPod ឥ្សរិយនៅរស់។</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Pêşdebiriyê teref bike</string>
<string name="upgrade_foss_sponsor_subtitle">Reklam tune. Śopandin tune. Girêdana Google Play tune.</string>
<string name="upgrade_foss_sponsor_returned_early">Zirû vegeriya? Piştgiriya we CAPod sax dihêle.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ಅಭಿವೃದ್ಧಿಗೆ ಪ್ರಾಯೋಜಕತ್ವ ನೀಡಿ</string>
<string name="upgrade_foss_sponsor_subtitle">ಜಾಹೀರಾತುಗಳಿಲ್ಲ. ಭಕ್ಷಣೆ ಇಲ್ಲ. Google Play ಲಾಕ್-ಇನ್ ಇಲ್ಲ.</string>
<string name="upgrade_foss_sponsor_returned_early">ಇಷ್ಟೇ ಹಿಂದಿರುಗಿದಿರಿ? ನಿಮ್ಮ ಬೆಂಬಲ CAPod ಅನ್ನು ಜೀವಂತವಾಗಿಡುತ್ತದೆ.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">개발 후원하기</string>
<string name="upgrade_foss_sponsor_subtitle">광고 없음. 추적 없음. Google Play 의존 없음.</string>
<string name="upgrade_foss_sponsor_returned_early">벌써 돌아가세요? 여러분의 지원이 CAPod를 유지합니다.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Иштелеп чыгууну колдоо</string>
<string name="upgrade_foss_sponsor_subtitle">Жарнама. Кадалоолоо жок. Google Playга байланма.</string>
<string name="upgrade_foss_sponsor_returned_early">Артка кайттыңызбы? Сиздин колдооңуз CAPodты тирүү сактайт.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ສະໜັບສະໜູນການພັດທະນາ</string>
<string name="upgrade_foss_sponsor_subtitle">ບໍ່ມີໂຄສະນາ. ບໍ່ມີການຕິດຕາມ. ບໍ່ຕິດ Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">ກັບມາແລ້ວ? ການສະໜັບສະໜູນຂອງທ່ານຮັກສາ CAPod ໃຫ້ດຳເນີນຕໍ່ໄປ.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Remti plėtrą</string>
<string name="upgrade_foss_sponsor_subtitle">Jokių reklamų. Jokio sekimo. Jokio „Google Play“ priklausomybės.</string>
<string name="upgrade_foss_sponsor_returned_early">Jau grįžte? Jūsų parama palaiko CAPod gyvybę.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsorēt attīstību</string>
<string name="upgrade_foss_sponsor_subtitle">Bez reklamām. Bez seklōšanas. Bez Google Play saistībām.</string>
<string name="upgrade_foss_sponsor_returned_early">Jau atpakaļ? Jūsu atbalsts uztur CAPod pie dzīvības.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Спонзорирај развој</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклами. Без пратење. Без зависност од Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Веќе се вративте? Вашата поддршка го одржува CAPod жив.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">വികസനം സ്പോൺസർ ചെയ്യുക</string>
<string name="upgrade_foss_sponsor_subtitle">പരസ്യങ്ങൾ ഇല്ല. ട്രാക്കിംഗ് ഇല്ല. Google Play ലോക്ക്-ഇൻ ഇല്ല.</string>
<string name="upgrade_foss_sponsor_returned_early">ഇത്ര പെട്ടെന്ന് തിരിച്ചു പോകുന്നോ? നിങ്ങളുടെ പിന്തുണ CAPod നിലനിർത്തുന്നു.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Хөгжлийг дэмжих</string>
<string name="upgrade_foss_sponsor_subtitle">Реклам байхгүй. Хяналт байхгүй. Google Play-д хамааралгүй.</string>
<string name="upgrade_foss_sponsor_returned_early">Аль хэдэйн буцаж ирлээ? Таны дэмжлэг CAPod-ийг амьд байлгадаг.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">विकासास प्रायोजित करा</string>
<string name="upgrade_foss_sponsor_subtitle">कोणत्याही जाहिराती नाहीत. कोणतेही ट्रॅकिंग नाही. Google Play चे बंधन नाही.</string>
<string name="upgrade_foss_sponsor_returned_early">आधीच परत? तुमचा पाठिंबा CAPod जिवंत ठेवतो.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Pembangunan penaja</string>
<string name="upgrade_foss_sponsor_subtitle">Tiada iklan. Tiada penjejakan. Tiada ikatan Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Sudah kembali? Sokongan anda mengekalkan CAPod.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ဖွံ့ဖြိုးတိုးတက်မှုကို ကမကထပံ့ပိုးရန်</string>
<string name="upgrade_foss_sponsor_subtitle">ကြောင်ညာမရှိ။ ခြေရာမခံ။ Google Play ချုပ်ချယ်မြုမရှိ။</string>
<string name="upgrade_foss_sponsor_returned_early">ပြန်သွားပြီးလါ? သင်၏ ပံ့ပိုးမှု CAPod ကို ရှင်သန်ဆက်လက်ကေစေသည်။</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Støtt utviklingen</string>
<string name="upgrade_foss_sponsor_subtitle">Ingen reklame. Ingen sporing. Ingen Google Play-avhengighet.</string>
<string name="upgrade_foss_sponsor_returned_early">Allerede tilbake? Din støtte holder CAPod i live.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">विकासलाई प्रायोजन गर्नुहोस्</string>
<string name="upgrade_foss_sponsor_subtitle">कुनै विज्ञापन छैन। कुनै ट्रैकिङ छैन। Google Playमा बाँधिएको छैन।</string>
<string name="upgrade_foss_sponsor_returned_early">पहिले नै फिर्नुभयो? तपाईंको सहयोगले CAPod जीवित राख्छ।</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsor ontwikkeling</string>
<string name="upgrade_foss_sponsor_subtitle">Geen advertenties. Geen tracking. Geen Google Play-vergrendeling.</string>
<string name="upgrade_foss_sponsor_returned_early">Alweer terug? Jouw steun houdt CAPod in leven.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Wesprzyj rozwój</string>
<string name="upgrade_foss_sponsor_subtitle">Bez reklam. Bez śledzenia. Bez uzależnienia od Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Już wróciłeś? Twoje wsparcie utrzymuje CAPod przy życiu.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Patrocinador do desenvolvimento</string>
<string name="upgrade_foss_sponsor_subtitle">Sem anúncios. Sem rastreamento. Sem dependência do Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Já voltou? Seu apoio mantém o CAPod vivo.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Patrocine o desenvolvimento</string>
<string name="upgrade_foss_sponsor_subtitle">Sem anúncios. Sem rastreamento. Sem dependência do Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Já está a sair? O seu apoio mantém o CAPod ativo.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsurisar il sviluppament</string>
<string name="upgrade_foss_sponsor_subtitle">Nagins annunzis. Nagin tracking. Nagin blocadi da Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Gia enavos? Tes sustegn mantegn CAPod viv.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsorizează dezvoltarea</string>
<string name="upgrade_foss_sponsor_subtitle">Fara reclame. Fara urmarire. Fara dependenta de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ati revenit deja? Sprijinul dvs. mentine CAPod in viata.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+3 -2
View File
@@ -3,8 +3,9 @@
<string name="foss_upgrade_donate_label">Пожертвовать</string>
<string name="foss_upgrade_alreadydonated_label">Я уже пожертвовал</string>
<string name="foss_upgrade_no_money_label">Я потратил все свои деньги на AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS бесплатный и с открытым исходным кодом. Если вам полезно приложение, рассмотрите возможность спонсировать разработку, чтобы помочь проекту продолжать развиваться.</string>
<string name="upgrade_foss_preamble">CAPod FOSS бесплатный и с открытым исходным кодом. Если Вам полезно приложение, рассмотрите возможность спонсировать разработку, чтобы помочь проекту продолжать развиваться.</string>
<string name="upgrade_foss_sponsor_action">Поддержать разработку</string>
<string name="upgrade_foss_sponsor_subtitle">Без рекламы. Без слежки. Без привязки к Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Вернулись уже? Ваша поддержка помогает CAPod жить.</string>
<string name="upgrade_foss_sponsor_returned_early">Вернулись? Ваша поддержка помогает CAPod жить.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsoriza s\'isvilupu</string>
<string name="upgrade_foss_sponsor_subtitle">Peroe publicidade. Peroe trachiamentu. Peroe dipendentzia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Zas torradas giai? Su sostegnu tuyu mantenet CAPod in vida.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">සංවර්ධනයට අනුග්‍රහය දෙන්න</string>
<string name="upgrade_foss_sponsor_subtitle">අළු නැත. ට්‍රැකිං නැත. Google Play සලක඾ත්වය නැත.</string>
<string name="upgrade_foss_sponsor_returned_early">එක්බේන්මට පළ්ට? ඔබ෪් සහය් CAPod ක්‍රියාත්මකව තබා ගෙනයි.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponzorovaný vývoj</string>
<string name="upgrade_foss_sponsor_subtitle">Žiadne reklamy. Žiadne sledovanie. Žiadna závislosť od Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Už späť? Vaša podpora udržiava CAPod pri živote.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Podpri razvoj</string>
<string name="upgrade_foss_sponsor_subtitle">Brez oglasov. Brez sledenja. Brez vezave na Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Že nazaj? Vaša podpora ohranja CAPod pri življenju.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Zhvillimi i sponsorëve</string>
<string name="upgrade_foss_sponsor_subtitle">Pa reklama. Pa gjurmim. Pa varësi nga Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ktheheni tashmjë? Mbështjetja juaj e mban CAPod gjallë.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Спонзориши развој</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклама. Без праћења. Без зависности од Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Већ одлазите? Ваша подршка чува CAPod живим.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsra utvecklingen</string>
<string name="upgrade_foss_sponsor_subtitle">Inga annonser. Ingen spårning. Ingen bindning till Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Redan tillbaka? Ditt stöd håller CAPod vid liv.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Faidhi maendeleo</string>
<string name="upgrade_foss_sponsor_subtitle">Hakuna matangazo. Hakuna ufuatiliaji. Hakuna kufungwa kwa Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Unarudi tayari? Msaada wako unaweka CAPod iwe hai.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">மேம்பாட்டை ஸ்பான்சர் செய்யுங்கள்</string>
<string name="upgrade_foss_sponsor_subtitle">ஐவிளம்பரம் இல்லை. கண்காணிப்பு இல்லை. Google Play கட்டுப்பாட்டு இல்லை.</string>
<string name="upgrade_foss_sponsor_returned_early">இத்தனை இருக்கிறீரகளா? உங்கள் ஆதரவு CAPodஐ உயிருடன் வைத்திருக்கிறது.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">అభివృద్ధిని స్పాన్సర్ చేయండి</string>
<string name="upgrade_foss_sponsor_subtitle">జాహీరాతులు లేవు. ట్రాకింగ్ లేదు. Google Play ఒక్కటికి పరిమితం కాదు.</string>
<string name="upgrade_foss_sponsor_returned_early">ఇర్వై వెనక్కి వెళ్ళారా? మీ మద్దతు CAPodను సజీవంగా ఉంచుతుంది.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">สปอนเซอร์การพัฒนา</string>
<string name="upgrade_foss_sponsor_subtitle">ไม่มีโฆษณา ไม่ติดตาม ไม่ผูกขาด Google Play</string>
<string name="upgrade_foss_sponsor_returned_early">กลับมาแล้ว? การสนับสนุนของคุณช่วยให้ CAPod ดำเนินต่อไป</string>
<string name="upgrade_badge_label">บังคับ</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Geliştirmeyi destekleyin</string>
<string name="upgrade_foss_sponsor_subtitle">Reklam yok. Takip yok. Google Play sınırlaması yok.</string>
<string name="upgrade_foss_sponsor_returned_early">Şimdiden döndün mü? Desteğin CAPod\'u ayakta tutuyor.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Розвиток спонсорів</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклами. Без стеження. Без прив\'язки до Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Вже повертаєтеся? Ваша підтримка зберігає CAPod.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">ترقی کو اسپانسر کریں</string>
<string name="upgrade_foss_sponsor_subtitle">کوئی اشتہار نہیں۔ کوئی ٹریکنگ نہیں۔ کوئی Google Play کی قید نہیں۔</string>
<string name="upgrade_foss_sponsor_returned_early">پہلے ہی واپس؟ آپ کی مدد CAPod کو زندہ رکھتی ہے۔</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Rivojlanishni sponsorlik qilish</string>
<string name="upgrade_foss_sponsor_subtitle">Reklamasiz. Kuzatishsiz. Google Play cheklovlarisiz.</string>
<string name="upgrade_foss_sponsor_returned_early">Allaqachon qaytdingizmi? Sizning yordamingiz CAPod\'ni tirik saqlaydi.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Tài trợ nhà phát triển</string>
<string name="upgrade_foss_sponsor_subtitle">Không quảng cáo. Không theo dõi. Không phụ thuộc Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Quay lại rồi sao? Sự ủng hộ của bạn giúp CAPod tiếp tục tồn tại.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">赞助开发</string>
<string name="upgrade_foss_sponsor_subtitle">无广告。无追踪。无 Google Play 绑定。</string>
<string name="upgrade_foss_sponsor_returned_early">已经回去了?您的支持是 CAPod 生存的动力。</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">贊助開發</string>
<string name="upgrade_foss_sponsor_subtitle">沒有廣告。沒有追蹤。沒有 Google Play 鎖定。</string>
<string name="upgrade_foss_sponsor_returned_early">這麼快就要離開?您的支持讓 CAPod 繼續存活。</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">贊助開發</string>
<string name="upgrade_foss_sponsor_subtitle">無廣告。無追蹤。無 Google Play 鎖定。</string>
<string name="upgrade_foss_sponsor_returned_early">已經回容了?您的支持讓 CAPod 繼續運行。</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Xhasa ukuthuthukiswa</string>
<string name="upgrade_foss_sponsor_subtitle">Azikho izibhangeli. Akunakulandelelwa. Ayikho ukuvinjwa kwe-Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ubuyile? Ukwesekwa kwakho kugcina i-CAPod iphila.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrade_foss_sponsor_action">Sponsor development</string>
<string name="upgrade_foss_sponsor_subtitle">No ads. No tracking. No Google Play lock-in.</string>
<string name="upgrade_foss_sponsor_returned_early">Back already? Your support keeps CAPod alive.</string>
<string name="upgrade_badge_label">FOSS</string>
</resources>
@@ -7,14 +7,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -27,6 +24,8 @@ import androidx.compose.material.icons.twotone.Favorite
import androidx.compose.material.icons.twotone.Palette
import androidx.compose.material.icons.twotone.PlayCircle
import androidx.compose.material.icons.twotone.Stars
import androidx.compose.material.icons.twotone.Headphones
import androidx.compose.material.icons.twotone.Tune
import androidx.compose.material.icons.twotone.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@@ -37,6 +36,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -122,125 +122,140 @@ fun UpgradeScreen(
val benefits = listOf(
Benefit(Icons.TwoTone.Palette, R.string.upgrade_benefit_themes),
Benefit(Icons.TwoTone.PlayCircle, R.string.upgrade_benefit_autoplay),
Benefit(Icons.TwoTone.BluetoothConnected, R.string.upgrade_benefit_autoconnect),
Benefit(Icons.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
Benefit(Icons.TwoTone.Tune, R.string.upgrade_benefit_device_settings),
Benefit(Icons.TwoTone.Headphones, R.string.upgrade_benefit_device_controls),
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
)
Box(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(16.dp))
Box(contentAlignment = Alignment.Center) {
Surface(
modifier = Modifier.size(120.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
) {}
Image(
painter = painterResource(R.drawable.splash_graphic2),
contentDescription = null,
modifier = Modifier.size(80.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = buildAnnotatedString {
append("CAPod ")
withStyle(SpanStyle(color = colorResource(R.color.brand_tertiary), fontWeight = FontWeight.Bold)) {
append("Pro")
}
},
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(24.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
Scaffold(
containerColor = MaterialTheme.colorScheme.surface,
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(48.dp))
Box(contentAlignment = Alignment.Center) {
Surface(
modifier = Modifier.size(120.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
) {}
Image(
painter = painterResource(R.drawable.splash_graphic2),
contentDescription = null,
modifier = Modifier.size(80.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.upgrade_preamble),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
text = buildAnnotatedString {
append("CAPod ")
withStyle(SpanStyle(color = colorResource(R.color.brand_tertiary), fontWeight = FontWeight.Bold)) {
append("Pro")
}
},
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
benefits.forEach { benefit ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(28.dp),
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.upgrade_preamble),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
benefits.forEach { benefit ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(16.dp),
)
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(28.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(16.dp),
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(benefit.textRes),
style = MaterialTheme.typography.bodyLarge,
)
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(benefit.textRes),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
Text(
text = stringResource(R.string.upgrade_benefit_disclaimer),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp),
)
Spacer(modifier = Modifier.height(24.dp))
if (state == null) {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
} else {
PricingContent(
state = state,
onSubscription = onSubscription,
onSubscriptionTrial = onSubscriptionTrial,
onIap = onIap,
onRestore = onRestore,
)
}
Spacer(modifier = Modifier.height(24.dp))
}
Spacer(modifier = Modifier.height(24.dp))
if (state == null) {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
} else {
PricingContent(
state = state,
onSubscription = onSubscription,
onSubscriptionTrial = onSubscriptionTrial,
onIap = onIap,
onRestore = onRestore,
IconButton(
onClick = onNavigateUp,
modifier = Modifier
.align(Alignment.TopStart)
.padding(4.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
)
}
Spacer(modifier = Modifier.height(24.dp))
}
IconButton(
onClick = onNavigateUp,
modifier = Modifier.padding(4.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@@ -3,6 +3,7 @@ package eu.darken.capod.upgrade.ui
import android.app.Activity
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider
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.asLog
import eu.darken.capod.common.debug.logging.log
@@ -116,18 +117,22 @@ class UpgradeViewModel @Inject constructor(
}
fun onGoIap() {
log(TAG, INFO) { "onGoIap()" }
billingEvents.tryEmit(BillingEvent.LaunchIap)
}
fun onGoSubscription() {
log(TAG, INFO) { "onGoSubscription()" }
billingEvents.tryEmit(BillingEvent.LaunchSubscription)
}
fun onGoSubscriptionTrial() {
log(TAG, INFO) { "onGoSubscriptionTrial()" }
billingEvents.tryEmit(BillingEvent.LaunchSubscriptionTrial)
}
fun launchBillingIap(activity: Activity) = launch {
log(TAG, INFO) { "launchBillingIap()" }
try {
withContext(dispatcherProvider.Main) {
upgradeRepo.launchBillingFlow(activity, CapodSku.Iap.PRO_UPGRADE)
@@ -139,6 +144,7 @@ class UpgradeViewModel @Inject constructor(
}
fun launchBillingSubscription(activity: Activity) = launch {
log(TAG, INFO) { "launchBillingSubscription()" }
try {
withContext(dispatcherProvider.Main) {
upgradeRepo.launchBillingFlow(
@@ -154,6 +160,7 @@ class UpgradeViewModel @Inject constructor(
}
fun launchBillingSubscriptionTrial(activity: Activity) = launch {
log(TAG, INFO) { "launchBillingSubscriptionTrial()" }
try {
withContext(dispatcherProvider.Main) {
upgradeRepo.launchBillingFlow(
@@ -169,9 +176,10 @@ class UpgradeViewModel @Inject constructor(
}
fun restorePurchase() = launch {
log(TAG, INFO) { "restorePurchase()" }
try {
val data = upgradeRepo.refresh()
log(TAG) { "Restore check: $data" }
log(TAG, INFO) { "Restore check: $data" }
val info = upgradeRepo.upgradeInfo.first()
if (info.isPro) {
log(TAG) { "Pro purchase found" }
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Google Play-faktureringsfout</string>
<string name="upgrades_gplay_billing_result_error_description">Daar was \'n fout toe Google Play om jou aankoopbesonderhede gevra is. Maak die Google Play-kas skoon en herbegin jou foon.\n\nFout %s</string>
<string name="upgrade_restore_action">Herstel aankoop</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">የGoogle Play ክፍያ ስህተት</string>
<string name="upgrades_gplay_billing_result_error_description">Google Play የግዢ ዝርዝሮችዎን ሲጠይቅ ስህተት ነበር። የGoogle Play መሸጎጫውን ያጽዱ እና ስልክዎን እንደገና ያስጀምሩ።\n\nስህተት %s</string>
<string name="upgrade_restore_action">ግዢን ወደነበረበት መልስ</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">خطأ في فوترة Google Play</string>
<string name="upgrades_gplay_billing_result_error_description">حدث خطأ عند طلب تفاصيل الشراء من Google Play. امسح ذاكرة التخزين المؤقت لـ Google Play وأعد تشغيل هاتفك.\n\nخطأ %s</string>
<string name="upgrade_restore_action">استعادة المُشتَرَى</string>
<string name="upgrade_badge_label">احترافي</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Google Play faktura xətası</string>
<string name="upgrades_gplay_billing_result_error_description">Google Play-dən satınalma təfərrüatlarınızı soruşarkən xəta baş verdi. Google Play keşini təmizləyin və telefonunuzu yenidən başladın.\n\nXəta %s</string>
<string name="upgrade_restore_action">Alışı bərpa et</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Памылка выстаўлення рахункаў Google Play</string>
<string name="upgrades_gplay_billing_result_error_description">Адбылася памылка пры запыце дэталяў пакупкі ў Google Play. Ачысціце кэш Google Play і перазагрузіце тэлефон.\n\nПамылка %s</string>
<string name="upgrade_restore_action">Аднавіць пакупку</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Грешка при фактуриране в Google Play</string>
<string name="upgrades_gplay_billing_result_error_description">Възникна грешка при заявката за данни за покупката ви от Google Play. Изчистете кеша на Google Play и рестартирайте телефона си.\n\nГрешка %s</string>
<string name="upgrade_restore_action">Възстановете покупката</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Google Play বিলিং ত্রুটি</string>
<string name="upgrades_gplay_billing_result_error_description">Google Play-তে আপনার কেনাকাটার বিবরণ জানতে চাইলে একটি ত্রুটি হয়েছে। Google Play ক্যাশে সাফ করুন এবং আপনার ফোন রিবুট করুন।\n\nত্রুটি %s</string>
<string name="upgrade_restore_action">ক্রয় পুনরুদ্ধার</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Error de facturació del Google Play</string>
<string name="upgrades_gplay_billing_result_error_description">S\'ha produït un error en demanar al Google Play els detalls de la vostra compra. Netegeu la memòria cau del Google Play i reinicieu el telèfon.\n\nError %s</string>
<string name="upgrade_restore_action">Restaura la compra</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Chyba fakturacie Google Play</string>
<string name="upgrades_gplay_billing_result_error_description">Při dotazování Google Play na podrobnosti o nákupu došlo k chybě. Smažte mezipaměť Google Play a restartujte telefon.\n\nChyba %s</string>
<string name="upgrade_restore_action">Obnovit nákup</string>
<string name="upgrade_badge_label">Pro</string>
</resources>
+1
View File
@@ -7,4 +7,5 @@
<string name="upgrades_gplay_billing_result_error_label">Google Play-faktureringsfejl</string>
<string name="upgrades_gplay_billing_result_error_description">Der opstod en fejl under forespørgsel af dine købsoplysninger fra Google Play. Ryd Google Play-cachen, og genstart din telefon.\n\nFejl %s</string>
<string name="upgrade_restore_action">Gendan køb</string>
<string name="upgrade_badge_label">Pro</string>
</resources>

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