Compare commits

..
Author SHA1 Message Date
Matthias Urhahn 911a09c743 fix(permissions): Offer battery optimization card in AUTOMATIC mode 2026-07-17 17:24:48 +02:00
Matthias Urhahn 2672dbdcf6 feat(monitor): Detect OS kills of the monitor and show a recovery hint 2026-07-17 17:24:41 +02:00
d4rken-org-releaser[bot] 3ddd5f3cfd Release: 5.2.1-rc0 2026-07-08 11:21:18 +00:00
Matthias Urhahn 31f4d4aafa fix(battery): Stop remaining-time jumping up when toggling ANC
A mode's own drain-rate bucket starts empty until it accumulates history,
so toggling ANC into an unlearned mode fell straight through to Apple's
optimistic spec rating while the mode just left showed its worse measured
rate. Result: enabling ANC could make the displayed time jump up ~1h.

Fill an empty ANC bucket at display time with the less-optimistic of the
mode-agnostic UNKNOWN reading and a sibling mode's learned rate, scaled by
the ratio of the two modes' rated drain. Scoped to spec'd models and
device-supported modes; picks the best-evidenced sibling, tie-broken by
closest rated drain then recency. No persistence or UI change.

The existing spec ceiling and display clamp still backstop the borrowed rate.
2026-07-07 22:23:01 +02:00
Matthias Urhahn e06dc933b8 chore(logging): Log BLE scan reception nanos in scan summaries 2026-07-06 12:42:10 +02:00
Matthias Urhahn 1418be999d feat(reaction): Time-cap auto-pause debounce on slow BLE scanners 2026-07-06 12:42:10 +02:00
d4rken-org-releaser[bot] df06a49949 Release: 5.2.0-rc0 2026-07-03 10:53:28 +00:00
darken b6dcc74383 fix: Hide charging indicator for disconnected devices 2026-07-03 12:43:33 +02:00
darken fead8381bc General: Update app translations from Crowdin 2026-07-03 11:25:05 +02:00
darken 2ab9f8ace6 chore(screenshots): Show battery estimates in dashboard screenshots 2026-07-02 17:32:29 +02:00
darken 81effcb349 chore(screenshots): Regenerate smoke locale screenshots 2026-07-02 17:32:29 +02:00
darken 3dd1f8151a fix(widget): Correct typo in the configuration resize hint 2026-07-02 17:32:29 +02:00
darken 272a7121a9 fix(screenshots): Use resource-qualifier locales for region-coded languages 2026-07-02 17:32:29 +02:00
darken 52a7850c1d feat(battery): Show battery estimates in widget and expanded notification 2026-07-02 17:32:29 +02:00
darken b297ad8564 ui(battery): Simplify the health placeholder text 2026-07-02 17:32:29 +02:00
darken a3810353f7 Revert "feat(battery): Add experimental case charge ETA and case battery health"
This reverts commit 87a9881024.
2026-07-02 17:32:29 +02:00
darken d4dddb1f38 feat(battery): Add experimental case charge ETA and case battery health
- Show the time until the case is full inside its charging chip, learned from
  the case's own rising level with the existing charge-band model; no rating
  exists for case charging, so the first charge learns before it shows
- Derive a case battery health from observed transfer efficiency: pod percent
  gained per case percent spent while docked and unplugged, corrected by each
  pod's own health, compared against Apple's "with charging case" totals
- Case data is only genuine while a pod is docked — both transports silently
  freeze the last value otherwise. Battery updates now flag whether the case
  entry is live, and BLE gains strict same-frame case accessors, so estimates
  never learn from frozen echoes
- The case deliberately gets no runtime estimate: idle-then-burst drain has no
  meaningful hourly rate
- Mark the case metrics as experimental in the Battery settings card
2026-07-02 17:32:29 +02:00
darken 21560a1f3a ui(battery): Tighten estimate setting titles and descriptions 2026-07-02 17:32:29 +02:00
darken bdfb6bb073 ui(battery): Match the divider style of the sibling settings cards 2026-07-02 17:32:29 +02:00
darken aac0590017 ui(battery): Surface health before data exists, reorder the Battery card
- Show a "Still determining — check back after a few more listening sessions"
  placeholder under Battery Health in the device info sheet while the estimate
  is still accumulating data, so the feature is discoverable from day one; no
  placeholder for profiles without a paired device, where it could never resolve
- Reorder the Battery card: time remaining & health settings first, then a
  divider, then the charging-side settings (charge limit, charged notification)
- Mention the battery-health component in the estimate toggle and reset texts
- Ground the charge-band boundaries in Apple's documented fast-to-80%/trickle
  charging behavior and standard lithium CC/CV charging references
2026-07-02 17:32:29 +02:00
darken afba33dd83 feat(battery): Model charge taper per band, base health on listening-only drain
- Replace the single linear charge rate with a three-band model (bulk / taper /
  trickle) matching lithium CC/CV charging: each band learns its own rate, the
  ETA walks the remaining bands, and the spec seed gets a taper haircut for the
  slow bands — no more over-promising above 80%
- Base the battery-health figure exclusively on drain observed while the pod is
  worn, audio is playing, AND this device is the system's audio sink; idle wear
  previously diluted health upward against Apple's listening ratings
- Listening segments are flushed for persistence the moment their gate breaks
  (playback stop, docking, transport flip) instead of being discarded with the
  cleared window
- The time-remaining estimate keeps learning from all usage — actual current
  drain, idle included, is the right basis for "how long will they last"
2026-07-02 17:32:29 +02:00
darken 5de5dee52f ui(battery): Show charge ETA in the charging chip, make health per-pod
- The gauge line under the percentage now always shows the runtime estimate
  ("if used now") even while charging; the time-until-charged moved into the
  charging chip itself ("Charging · 25m"), so the two can't be confused
- Battery health is now computed and shown per pod (Left/Right paired row in
  the info sheet, mirroring the serial rows) — single-pod listening habits or
  a replaced earbud make the sides genuinely diverge, and a combined figure
  would mask a failing pod
2026-07-02 17:32:29 +02:00
darken ba3e253569 fix(battery): Seed charge ETA from quick-charge specs, hide projection while charging
- While charging, the gauge showed the bare runtime projection when no charge
  rate existed yet — "1% · 4m" next to a charging chip reads as a four-minute
  charge. The line now shows the "until charged" ETA or nothing
- Seed the charge rate from Apple's published quick-charge claims ("5 minutes
  in the case provides around 1 hour of listening"), normalized against the
  rated listening hours, so the ETA is present from the very first charge; a
  live fit still takes over within minutes
2026-07-02 17:32:29 +02:00
darken 4488af0ab7 feat(battery): Add time-until-charged and derived battery health
- While a pod charges, fit its rising level and show the time until full in the
  gauge instead of the runtime estimate; learned charge rates are persisted per
  slot so the ETA appears immediately on later charges
- Suppress the charge ETA during Optimized Battery Charging holds, the final
  trickle phase, and whenever the level stalls longer than one visible step
  should take (granularity-aware: 1% AAP steps vs 10% BLE steps)
- Clear a slot's fit window when its readings switch between AAP and BLE — the
  granularity jump would otherwise read as a fake level step
- Derive a battery-health percentage (median of accumulated drain rates vs the
  model's rated life) and show it in the device info sheet; the info button now
  also appears for BLE-only devices once health data exists
- Tag learned rates with the model they came from so re-pointing a profile at
  different hardware starts learning fresh instead of inheriting foreign rates
- Track how many sessions blended into each learned rate and require three
  before a health figure is shown
2026-07-02 17:32:29 +02:00
darken aae8ad62bd feat(battery): Make estimate per-device and seed from model specs
- Replace the global estimate toggle with a per-device toggle stored on the profile
- Seed the estimate from each model's rated battery life and show it immediately, using
  the rating as a hard upper bound on displayed life while the measured rate converges
- When the ANC mode is unknown, seed from the shorter of a model's ANC-on/off ratings
- Show a projection while charging ("if used now") without ever learning from a rising battery
- Consolidate charge limit, "notify when charged", the estimate toggle and reset into one
  Battery card; the charge notification now works for any live device, not only classic
  audio connections
- Smooth the displayed time asymmetrically (drop fast, rise slow) so a faster-than-rated
  drain stops over-promising within a couple of updates
2026-07-02 17:32:29 +02:00
darken 9606a33f15 feat: Add earbud battery time-remaining estimate 2026-07-01 17:58:12 +02:00
darken f626d70538 fix(monitor): Show worn device in notification and popup 2026-06-30 23:29:34 +02:00
d4rken-org-releaser[bot] a91ef44686 Release: 5.1.10-rc0 2026-06-22 19:00:07 +00:00
darken e3bef4e74b General: Update app translations from Crowdin 2026-06-22 20:42:35 +02:00
darken 358bf5e244 fix(monitor): Drive monitor mode from first addressed profile 2026-06-22 20:16:37 +02:00
d4rken-org-releaser[bot] fba2d3a59d Release: 5.1.9-rc0 2026-06-18 12:00:29 +00:00
darken 0abba4ece2 test(reaction): Harden Conversational Awareness pod-removal coverage 2026-06-18 13:57:07 +02:00
darken e9b0694b67 fix(reaction): Honor One-Pod Mode in CA resume worn-guard 2026-06-18 12:07:19 +02:00
darken 49bbf1bc85 fix(reaction): Settle cold CA terminals to catch pod-removal re-key race 2026-06-18 12:07:19 +02:00
darken f5b6a5c605 fix(reaction): Ignore CA terminals from pod removal/insertion re-key 2026-06-18 12:07:19 +02:00
darken fccc6cedd5 fix(reaction): Restore CA status 6 as a terminal 2026-06-18 12:07:19 +02:00
darken a03766448e fix(reaction): Apply resume age-guard only to the inferred stale-end 2026-06-18 12:07:19 +02:00
darken f0c9d318ff fix(aap): Report speaking=true for Conversational Awareness resume (status 5) 2026-06-18 12:07:19 +02:00
darken 5c5cc6f28e fix(reaction): Refresh CA keep-alive timestamp so long conversations resume 2026-06-18 12:07:19 +02:00
darken 246b96bfb4 fix(reaction): Treat CA status 5 as speech-resume, not a stop 2026-06-18 12:07:19 +02:00
darken 352e020b54 test(aap): Make handshake-timeout test deterministic with real-time bounds 2026-06-15 09:16:51 +02:00
darken 72f5690ea6 test(aap): Stabilize flaky handshake-timeout test under CI load 2026-06-15 09:16:51 +02:00
darken 1ed1199574 fix(ble): Stop foreign same-model pods from hijacking a device's identity 2026-06-15 09:16:51 +02:00
darken fd4a43de96 fix(aap): Bound handshake with watchdog and escalate reconnect backoff 2026-06-14 21:28:42 +02:00
darken e406fd4cc1 fix(monitor): Restore tint on product icons to prevent invisibility on light backgrounds
Without tint, white PNG product icons (AirPods photos) are invisible on
light notification backgrounds. Restore src_in tint with notification_icon_tint
(a reliable day/night color resource) in both the PodInfoItemIcon.Notification
style (covers small layouts) and the big layout product ImageViews that do
not use that style.
2026-06-14 17:52:45 +02:00
darken 1e555cc712 fix(monitor): Fix notification color inconsistency in dark mode on One UI
Remove Theme.Material3.DynamicColors.DayNight from the notification
container style; it does not resolve correctly in the notification host
process, causing light-mode colors to appear in dark mode. Replace all
?attr/colorAccent and ?android:attr/textColorPrimary refs in RemoteViews
layouts with explicit day/night color resources (notification_icon_tint)
and TextAppearance.Compat.Notification.Title defaults. Change tintMode
from multiply to src_in for status glyphs; remove tint from product PNG
icons to prevent silhouette rendering.

Fixes #613
2026-06-14 17:52:45 +02:00
d4rken-org-releaser[bot] 227a6b2a7b Release: 5.1.8-rc0 2026-06-10 18:25:44 +00:00
darken 5f661d71a6 General: Update app translations from Crowdin 2026-06-10 20:20:37 +02:00
darken 2804543b9d feat(reaction): Add charged notification 2026-06-10 18:13:51 +02:00
darken 380397321c fix(reaction): Apply volume reduction slider only on drag release
The conversation volume slider committed a profile write on every
drag frame — laggy dragging, and racing async writes persisted stale
mid-drag values (observed: 78 stored while the UI showed 70). Use
local state while dragging and commit on release, matching the
adaptive-noise and tone-volume sliders.
2026-06-10 11:39:58 +02:00
darken d9c8902d34 fix(reaction): Restore volume when conversation end frame never arrives
With one AirPod in the case the pod deterministically drops the
terminal 0x4B frame: the wind-down flurry ends on a transitional
status (1,2,3,0xB,4 then nothing), so the reaction never disengaged
and the volume stayed low until the 5-minute backstop (#608).
Reproduced on Pro 3 (fw 6589, 6503) and Pro 2 USB-C (fw 6814) —
all three share one firmware train; with both pods worn the terminal
always arrives.

The pod sends no frames during active speech, so any non-START frame
means the wind-down has begun and a terminal is imminent. A HOLD now
arms a short 6s fuse instead of the 5-minute backstop: if no terminal
(or fresh START) follows, the reaction disengages anyway. The fuse
must stay above ~5s — gaps up to 2.8s were observed between
consecutive wind-down frames. STARTs still arm the long backstop,
since the pod stays engaged and silent against ambient noise for
20-30s+ and a short timeout there resumes media mid-conversation.

Validated on hardware on both models: 7/7 single-pod conversations
restored exactly 6s after the last wind-down frame.
2026-06-10 11:39:58 +02:00
darken a1b9abd4cb fix(ble): Drop proximity messages with unsupported payload prefix
AirPods Gen4 emit a one-off type 0x07 frame from their identity address
on connect whose payload prefix is 0x07 instead of 0x01. Its bytes are
not the plaintext status format, but the model bytes happen to match,
so it minted a fresh device tracker and showed as a duplicate device
card until the stale timeout.

Every known plaintext status broadcast (including pairing mode) uses
prefix 0x01; pairing state lives in the suffix byte. Reject anything
else at the decoder, logging the dropped frame's hex.

Fixes #603
2026-06-10 10:44:48 +02:00
d4rken-org-releaser[bot] 5d30844fa8 Release: 5.1.7-rc0 2026-06-08 07:22:52 +00:00
Matthias Urhahn af1b1b4249 Revert #605's cooldown-only mitigation in favor of the root-cause fix
@mvanhorn independently fixed #598 in #605 by no longer resetting the
case cooldown on close, throttling the re-pop. We cherry-picked that
commit above to keep his authorship/credit, but this PR instead removes
the underlying lid-state flapping at its source (out-of-case pod's stale
lid byte -> UNKNOWN), so the cooldown can keep resetting on close and a
genuine close->reopen still shows the popup. Reverting his change here so
the two approaches don't stack; thanks @mvanhorn for the parallel work.
2026-06-08 08:05:50 +02:00
Matt Van Horn 2cce9ae56c fix: case-open popup re-appears after the case is closed 2026-06-08 08:05:50 +02:00
Matthias Urhahn 4bab7c61a9 fix(reaction): Stop case popup flicker when one AirPod is out of case
When one pod is out of the case, the out-of-case pod broadcasts bit4-only
frames whose lid byte is stale and decodes to a phantom OPEN even while the
case is shut, interleaved ~1:1 with the correct in-case-pod (bit6) frames.
The derived lid state flapped OPEN<->CLOSED, so the case-open popup re-popped
~0.5s after closing and sometimes lingered. Verified on AirPods Pro 1 (issue
log) and Pro 3 (live BLE capture).

- Trust the lid bit only from in-case-pod (bit6) or both-in-case (bit2) frames;
  bit4-only frames now decode to UNKNOWN, and the real state is recovered from a
  recent in-case broadcast (matches LibrePods).
- getLatestCaseLidState recovers from history within a 2s age window instead of a
  fixed frame count, so a missed CLOSED can't keep a stale OPEN.
- Don't refresh the show-cooldown on a non-CLOSED hide, so a transient UNKNOWN
  can't suppress a genuine re-open.
- Add a freshness backstop: dismiss the popup if a fresh OPEN broadcast stops
  arriving (device left BLE range while open).
- Key popup/auto-connect de-duplication on the derived lid state, not just raw
  advertisement bytes, since the effective lid can change while bytes don't.

Closes #598
2026-06-08 08:05:50 +02:00
Matthias Urhahn 81e2bc1dba feat(overview): Suggest troubleshooter when a connected device sends no data
Adds a dashboard hint card pointing at the existing Troubleshooter when a profile is connected to the system (audio) but CAPod is receiving no live data — the symptom of a phone dropping AirPods BLE broadcasts (e.g. some HyperOS devices, #603). Debounced ~15s so it doesn't flash during the gap between an audio connection and the first broadcast, and suppressed whenever any pod is live so it never claims 'no data' while data is visibly arriving.
2026-06-08 07:45:29 +02:00
Matthias Urhahn df2783d8a1 feat(troubleshooter): Persist only the minimal working compat combo
Probe compatibility options through a transient in-memory override on BlePodMonitor instead of writing the user's persisted settings on every attempt. Only the winning combo is persisted, and only on success; failure or cancellation clears the override, restoring the user's original settings. Combos are tried fewest-disables-first so a phone that only needs batching disabled isn't left with filtering disabled too. Per-attempt cache clearing plus a freshness cutoff stop a previous combo's cached devices from satisfying the next one, and the 'found' checks now require a fresh live BLE observation rather than cached/AAP state.
2026-06-08 07:45:29 +02:00
Matthias Urhahn 20a6953b54 General: Update app translations from Crowdin 2026-06-07 13:30:43 +02:00
darken fdd4f69247 feat(overview): Add setting to hide unmatched devices 2026-05-31 10:04:26 +02:00
darken 5ee777b196 fix(reaction): Don't resume media mid-talk on Conversational Awareness
The pod only signals CA start and end, not continuous keep-alives. On fw 6861 it held CA engaged for 21s with zero 0x4B frames while the wearer kept talking, so the 12s stale-timeout fired mid-speech and resumed media; the pod never re-sent a start frame, so it didn't re-pause.

Disengage now waits for the explicit not-speaking frame (status 5 added as a terminal STOP, since that firmware winds down 3->5 and never reaches 6/8/9). Transitional/unknown statuses stay engaged. The stale timer is demoted to a long 5min backstop for a fully-dropped terminal frame; constants moved to Kotlin Durations.
2026-05-31 06:39:52 +02:00
d4rken-org-releaser[bot] dd827af3cd Release: 5.1.6-rc0 2026-05-26 13:32:39 +00:00
Matthias Urhahn 948205667b General: Update translations for 75 languages (#600)
* General: Update app translations from Crowdin

* General: Include latest Polish community translation updates
2026-05-26 12:20:17 +02:00
darken dbbfa75429 feat(reaction): Add Conversational Awareness media reaction
Reacts to the AirPods speaking-detection event (AAP 0x4B): lowers media volume by a configurable amount or pauses playback when you start talking, and reverts when you stop. Per-profile, Pro-gated, opt-in (default off).

Decodes the speaking status from the last payload byte ({1,2}=start, {6,8,9}=stop, else keep-alive); engage/disengage with a frame-idle stale timeout to recover a dropped stop, plus disconnect and service-stop cleanup.
2026-05-25 23:55:55 +02:00
Matthias Urhahn 78790f73df Delete Screenshot_20220914_174644.png~ 2026-05-11 12:39:16 +02:00
darken 2950d8b3f9 docs: Update README screenshots to current fastlane set 2026-05-11 12:37:41 +02:00
Matthias Urhahn 4d3a9bc165 refactor: Remove General Settings Debug menu and dead supporting code 2026-05-07 15:41:57 +02:00
Matthias Urhahn 7d57845502 fix(monitor): Make Extra notification toggle reactive 2026-05-07 15:23:06 +02:00
Matthias Urhahn 83ef111599 refactor: Move nudge persistence into GeneralSettings
Consolidate the nudge availability DataStoreValue with the rest of the persisted settings instead of carrying it in a separate Hilt module + DataStore file. Matches the existing convention where compat-style flags (offloaded filtering, indirect callbacks, etc.) live alongside theme/notification settings.

NudgeCapabilityStore keeps the verdict-mapping behavior; only the source of the persisted value changes.
2026-05-07 14:10:16 +02:00
Matthias Urhahn 0e6848d886 fix: Address Codex review on auto-monitor-mode
- Seed connectedDevices flow with onStart so ALWAYS autolaunch fires before HEADSET profile binds

- Gate UnavailableMissingPermission classification on Android 12+ (BLUETOOTH_CONNECT only exists from API 31)

- Treat blank/empty profile.address as unpaired (matches AutoConnect)

- Subscribe DeviceSettingsViewModel to nudgeCapabilityStore.availability so UI updates immediately on verdict change

- Seed availability StateFlow with the persisted value via valueBlocking to close the cold-start race
2026-05-07 14:10:16 +02:00
Matthias Urhahn 1eeeb63a76 test: Cover nudge persistence, permission gating, and autolaunch reactivity 2026-05-07 14:10:16 +02:00
Matthias Urhahn a4ba302223 refactor(monitor): Compute monitor mode automatically from profile state 2026-05-07 14:10:16 +02:00
Matthias Urhahn 8c9094d85a ui(reaction): Tighten Auto play / Start music on wear descriptions 2026-05-07 14:08:22 +02:00
Matthias Urhahn 4529b107dd fix(reaction): Only auto-resume after ear-removal pause, not stem or sleep
Matches Apple's iOS/macOS behavior: stem-press pauses and sleep-detection pauses are explicit user intent, not eligible for auto-resume on next pod-in. sendPause gains a rememberForResume parameter (default false); only PlayPause's auto-pause branch passes true. New sendStop wrapper clears the auto-resume flag for stem-mapped MEDIA_STOP.
2026-05-07 14:08:22 +02:00
Matthias Urhahn 17663bc759 refactor(reaction): Switch to sticky resume flag with opt-in cold-wear
Replaces the origin-tracking machinery with a simpler model that matches Apple's iOS/macOS behavior: auto-play strictly resumes a CAP-dispatched pause, gated by a sticky boolean cleared on inactive→active transitions. The original fire-on-cold-wear behavior is preserved as a per-device opt-in 'Start music on wear' setting.
2026-05-07 14:08:22 +02:00
Matthias Urhahn 278257998b fix(reaction): Don't auto-resume music after a user-initiated pause 2026-05-07 14:08:22 +02:00
d4rken-org-releaser[bot] 9d41979499 Release: 5.1.5-rc0 2026-05-06 10:16:00 +00:00
darken 985b73754e fix: Eliminate Float? boxing from battery display and cache merge
Two SIGSEGV native crashes recurred on Android 10 in 5.1.4-rc0 inside JIT-cached code at toBatteryFloat+4 (popup) and mergeBatterySlot+40 (cache merge). Both functions had a boxed Float? unbox at function entry that R8 horizontally merged into stdlib host classes, where Android 10 ART JIT miscompiled the unbox.

Convert PodDevice battery getters to non-null Float with BATTERY_UNKNOWN sentinel and propagate primitive Float through every display/persistence consumer. mergeBatterySlot now takes primitive Float; toBatteryFloat and toBatteryOrNull are deleted. Add isKnownBattery and batteryProgress helpers used everywhere instead of scattered nullable checks. Raw live extraction in toCachedState avoids touching the unified getter so cached values aren't refreshed as live.
2026-05-06 11:54:25 +02:00
Matthias Urhahn e4e4190523 General: Update zh-CN phone screenshots 2026-05-04 22:10:14 +02:00
Matthias Urhahn ebfb034fdd fix(navigation): Defer onNewIntent navCtrl call until Compose attaches
Move consumeUpgradeExtra() out of MainActivity.onNewIntent and into a Compose-level LaunchedEffect that collects a new MainActivity.warmIntents SingleEventFlow.

onNewIntent could fire before the setContent{} lambda registered the back stack with NavigationController, leaving navCtrl.goTo(Nav.Main.Upgrade) to throw IllegalStateException("NavigationController not initialized"). Same race shipped a fix for in octi (d4rken-org/octi#284).
2026-05-04 21:42:53 +02:00
Matthias Urhahn 8a81d354e6 chore(screenshots): Improve dashboard and add-profile previews
Use AAP-connected mocks for the dark dashboard to showcase full connectivity badges (BLE + IRK + encrypted + AAP) and the ANC mode selector. Populate the add-profile preview with a demo name, model, and paired device. Add user-facing labels to the dashboard mock devices.

Refactor BluetoothDevice2 so name/address are primary constructor fields, letting previews construct one without a real Android BluetoothDevice. Bump the screenshot test JVM heap to 4g — the smoke batch (42 renders) was hitting the test executor's default ceiling.
2026-05-04 21:30:25 +02:00
Matthias Urhahn ac0c4693dc chore(screenshots): Switch to smoke-only Play Store screenshot commits
Mirrors permission-pilot's policy: only the 6 smoke locales (en-US, de-DE, ja-JP, ar, zh-CN, pt-BR) check phoneScreenshots PNGs into the repo. Non-smoke locales are excluded via .gitignore. Drops fastlane/metadata/android/ from ~67 MB to ~7 MB and prevents future bloat from full regens. Play Store's supply retains previously-uploaded screenshots for locales not pushed, so full localization is maintained by occasional manual regen + screenshots_only upload.
2026-05-04 19:27:57 +02:00
Matthias Urhahn 4fd7a41c53 chore(screenshots): Regenerate Play Store screenshots
Replaces stale 7_settings.png and 8_reaction_settings.png with the current 7_device_settings_reactions.png across all 68 locales.
2026-05-04 18:50:30 +02:00
Matthias Urhahn cc1caf47f0 chore(fastlane): Add screenshots_only supply lane 2026-05-04 18:32:00 +02:00
darken 8b9f953818 feat(widget): Show profile name and nearby state when ANC widget is disconnected 2026-05-04 13:03:27 +02:00
d4rken-org-releaser[bot] d541de8c38 Release: 5.1.4-rc0 2026-05-03 17:32:42 +00:00
darken 2821da434d fix(settings): Hide Microphone & Press controls when AAP isn't ready 2026-05-03 19:27:52 +02:00
darken 07c72dc86c fix(aap): Cancel hung BluetoothSocket connect on timeout
BluetoothSocket.connect() is a blocking JNI call that ignores coroutine cancellation, so the previous withTimeout in AapAutoConnect only cancelled the suspending wrapper while the native thread stayed pinned. Hung threads accumulated and could trigger ANRs.

Move the timeout inside AapConnection and run the blocking connect on a daemon thread; on timeout, close the socket from the caller thread to unblock the native call (the documented Android pattern for cancelling in-flight L2CAP connects).

Also cancel appScope before delegating uncaught exceptions so coroutines have a best-effort window to release resources before the system handler terminates the process.
2026-05-03 15:39:03 +02:00
darken 7d93e772b4 fix(monitor): Prevent NPE in cache merge from freezing device flow
Battery slot percent comparisons in mergeBatterySlot/hasStateChanged compiled to Intrinsics.areEqual on boxed Float; R8 optimization on Android 10/11 dropped a null check during inlining and the resulting NPE escaped onEach { persistLiveDevices }, cancelling the upstream combine and freezing every observer of DeviceMonitor.devices.

Comparisons now operate on primitive float (cmpg-float in dex) so no Intrinsics.areEqual call remains in the merge path. The persist loop also catches and reports per-profile, and AAP-only profiles with active DeviceInfo are now persisted even when no BLE pod is in range.
2026-05-03 14:20:49 +02:00
darken 82dc88faac fix: Harden uncaught exception safety net
Wrap logging, reporting, and Looper resume calls so the foreground service timing exception suppression cannot itself trigger another crash. Extract handler into a dedicated class with seams for unit tests.
2026-05-03 12:41:32 +02:00
darken 06b41ec8eb fix(monitor): Recover from revoked Bluetooth scan permission 2026-05-03 11:54:11 +02:00
darken acc6e92b2d feat(tile): Add ANC Quick Settings tile 2026-05-03 10:58:05 +02:00
d4rken-org-releaser[bot] 39922df792 Release: 5.1.3-rc0 2026-05-01 19:52:12 +00:00
darken 5a8c4b94ef fix: Correct wear detection on AirPods Max 2
Bit 5 of pubStatus is always set on A3454 and no longer carries the wear flag (unlike Max gen 1). Read bits 1 and 3 instead — the per-earcup sensors. OR rather than AND so phones that only see one bit reliably still report worn correctly.

Closes #548
2026-05-01 21:50:51 +02:00
darken 0ffa87deea CI: Skip release bump-only workflow runs 2026-05-01 20:08:42 +02:00
darken fc98db09a2 fix: Correct PodModel feature flags for ear detection and call controls
Sync flags with what the BLE classes actually report and what iOS exposes:

- AirPods Gen 1/2/3: enable hasEarDetection (already parsed via DualApplePods) and hasEarDetectionToggle

- AirPods Gen 3, Pro 1: enable hasEndCallMuteMic (force-sensor stems)

- Powerbeats Pro, Beats Fit Pro: enable hasEarDetectionToggle (iOS exposes it)

- Beats Solo Pro, Studio 3: drop hasEarDetection (over-ear, BLE class is bare SingleApplePods)

- FAKE_AIRPODS_GEN1/2/3: enable hasEarDetection to match HasEarDetectionDual

- Generalize microphone mode description from 'AirPod' to 'earbud'

Tests rewritten as exhaustive set assertions plus implication invariants.
2026-05-01 20:08:29 +02:00
darken e1c0a702dd chore(ci): Rename stale step name and use client-id for App auth 2026-05-01 09:15:43 +02:00
d4rken-org-releaser[bot] 3f310016b1 Release: 5.1.2-rc1 2026-05-01 07:04:37 +00:00
darken d399a6d908 fix(ci): Drop redundant explicit dispatch in release-prepare 2026-05-01 09:00:29 +02:00
d4rken-org-releaser[bot] fb104cbe61 Release: 5.1.2-rc0 2026-05-01 06:56:07 +00:00
darken 97cbca7a6e fix(ci): Resolve bot slug via action output to avoid JWT-only endpoint 2026-05-01 08:55:15 +02:00
darken 02278dab17 chore(ci): Use GitHub App token for release-prepare push 2026-05-01 08:50:32 +02:00
darken 2240bd7ae5 chore(ci): Drop env gate on release-prepare to keep two-click approval 2026-05-01 08:16:33 +02:00
darken bc4096cb57 chore(ci): Run bats + shellcheck on release tooling 2026-05-01 08:00:15 +02:00
darken 5a0ba1b0bd chore(ci): Move release process to GitHub Actions 2026-05-01 08:00:15 +02:00
darken d04abdf80c General: Fix translation issues across multiple locales
Targeted character / escape / vandalism fixes:

- tr: %80 -> 80%% (UnknownFormatConversionException crash risk)

- pt: replace Cyrillic 'не' with 'não' in autoconnect description

- sk: fix 'ladUciaho' -> 'ladiaceho' (corruption)

- ms: restore 'Discord' brand name

- ca: 'maxima' -> 'minima' for signal_quality_title; fix 'suprimrira' typo

- zh-rTW: fix homophones and Japanese-kanji variants (4 strings)

- zh-rHK: Simplified -> Traditional for press_action_fast_forward

- zu: remove spurious 'Zn' prefix in 2 strings (vandalism)

- lv: fix wrong cedilla character in 3 Bluetooth a11y descriptions

- kmr-rTR: fix 4 encoding artifacts (a-feminine-ordinal substituted for e-circumflex)

- ko: fix 2 Hangul typos (conversation_awareness, allow_off)

Garbled translations removed (fall back to English source):

- hy-rAM: 13 press_controls/press_action strings

- mn-rMN: 2 strings (aap_unavailable_description, listening_mode_cycle_minimum)

- lt: settings_popup_warning_manual_mode

- mk-rMK: device_settings_allow_off_description (Syriac char vandalism)

- lo-rLA: signal_badge_ble_cd (English fallback was effectively unchanged)

- km-rKH: device_settings_microphone_mode_label (Thai script in Khmer file)
2026-05-01 05:19:34 +02:00
darken 0609352cc9 General: Update app translations from Crowdin 2026-05-01 05:19:34 +02:00
darken abde0b7d6a feat(reactions): Add discovery hint and outline settings icon 2026-04-30 17:46:27 +02:00
darken b952aff38d fix(widget): Re-render widgets reactively after initial session start
Glance only calls provideGlance() once per widget session; subsequent update() calls recompose the existing composition without re-running provideGlance. The previous capture-once approach left widgets frozen at initial state because the composition had no reactive State to read.

Subscribe to a widgetDeviceFlow(profileId) inside provideContent that pre-filters by WidgetDeviceKey, so the composition recomposes on visible state changes without firing on every BLE advertisement. Replace updateAll() with explicit per-GlanceId update() calls in WidgetManager, with a platform-id fallback when Glance returns no IDs.
2026-04-30 16:44:57 +02:00
darken c8f5ceda34 fix(monitor): Auto-tune BLE scanner mode by foreground state and connections
Replaces the user-facing scanner mode setting with an automatic policy that picks LOW_LATENCY when a profile-paired device is connected, BALANCED in the foreground, and LOW_POWER in the background. The TroubleShooter scopes a temporary LOW_LATENCY override via a refcounted withTemporaryOverride block so overlapping callers stay correct.

Fixes a regression where the controller could block BLE scanning entirely if BLUETOOTH_CONNECT was missing or the HEADSET profile proxy stalled, and adds a reactive bondedDeviceAddresses flow so bond changes propagate without waiting for an unrelated input. Cleans up the now-dead scanner mode strings across all locales and unused ScannerMode fields.
2026-04-30 15:26:45 +02:00
darken e6dbd2d660 fix(reaction): Restore BLE-only autoplay and stop false reactions on app start
- Apply seenLastAt freshness to all unauthenticated BLE samples (worn and not-worn). The earlier scoping to not-worn-only collapsed the second worn sample for BLE-only autoplay confirmation, so the staged play never fired.

- Replace distinctUntilChangedBy with a manual filter so worn samples that need to reset an active pause debounce (count went up) can pass through even when the monitor key is otherwise identical.

- Skip BLE-only autoplay confirmation for trusted sources. With BLE_IRK_MATCH and AAP, autoplay now fires on the first not-worn -> worn transition, mirroring the pause-debounce skip on the same sources.

- Skip the reaction entirely when the previous emission had no live evidence (NO_LIVE_BLE). Prevents app-process-start from synthesising a fake not-worn -> worn transition and firing autoplay while the user is already wearing the pods. Same guard handles mid-session BLE gap recoveries.

- Add MonitorFlowTests covering process-start-worn, genuine-insertion-after-startup, mid-session BLE-gap recovery, IRK-matched immediate autoplay, BLE-only autoplay confirmation, 3-sample pause debounce, and rebound-tolerated debounce reset.
2026-04-29 18:14:51 +02:00
darken 228d7d1f0c fix(reaction): Tighten auto-pause debounce after review findings
- Commit pending pause when a trusted source (AAP / BLE_IRK_MATCH) corroborates the not-worn condition mid-debounce, instead of dropping pending silently.

- Scope debounceFreshness to not-worn samples only; identical both-in samples no longer pass distinctUntilChangedBy and can't accidentally trigger BLE-only auto-play confirmation.

- Add resetTolerance to PendingPauseDebounce so a single corrupt count-up advert no longer kills a legitimate pending pause; reorder reset checks so rawDecision.shouldPlay resets immediately.

- Drop bleKeyState from the INFO autoPause log; source already encodes trust without leaking key-configuration state to logcat.

- Add flow-level MonitorFlowTests verifying the distinctUntilChangedBy interaction with seenLastAt freshness, plus the #557-direction test (AAP-worn vs corrupt-BLE-not-worn) and rebound-tolerance test.

- Clarify in BLE_ANONYMOUS KDoc that the path is unreachable in production via DeviceMonitor.primaryDevice.
2026-04-29 18:14:51 +02:00
darken 67fead3589 fix(reaction): Debounce auto-pause for unauthenticated BLE sources
Classifies the ear-detection source (AAP / BLE_IRK_MATCH / BLE_PROFILE_FALLBACK / BLE_ANONYMOUS / NO_LIVE_BLE) and applies a 3-sample debounce only to unauthenticated BLE paths. AAP and IRK-authenticated BLE pass through unchanged.

Also tightens toEarDetectionState() to prefer AAP aggregate over BLE per-side bits whenever AAP EarDetection is present, and suppresses pause on NO_LIVE_BLE (cache-only state) to avoid firing without live evidence.
2026-04-29 18:14:51 +02:00
darken d0aab583d2 fix(presscontrols): Auto-exit when device disconnects 2026-04-28 22:39:42 +02:00
darken 367c8d28ff chore: Drop stale app-common exclude from _config.yml
app-common module was merged into app/ in be8f4919; the exclude has been a no-op since.
2026-04-28 12:08:23 +02:00
darken b04e1fe628 chore(ci): Also rebuild Pages on main pushes for content edits
Adds push: [main] alongside workflow_dispatch so edits to README.md, _config.yml, _layouts, or the CHANGELOG.md template publish without a manual dispatch. The chain step in release-tag.yml still runs after release publish to guarantee the new release is in site.github.releases by the time Pages rebuilds — concurrency: cancel-in-progress: false serialises the two runs.

Adds an if: github.ref == 'refs/heads/main' guard on the deploy job so workflow_dispatch from a non-main branch builds for verification but doesn't deploy.
2026-04-28 12:08:23 +02:00
darken ea3ef1ff3b chore: Drop dead Gemfile excludes and ignore Jekyll artifacts
After moving Gemfile/Gemfile.lock to fastlane/, the - Gemfile / - Gemfile.lock entries in _config.yml's exclude list are no-ops; the parent fastlane exclude already covers everything inside.

.gitignore picks up _site/, .jekyll-cache/, vendor/bundle/ so local Jekyll runs don't leave tracked artifacts.
2026-04-28 12:08:23 +02:00
darken ff8b9cf6ed chore(ci): Chain Pages deploy from release-tag.yml and adopt org refinements
release: published events triggered by secrets.GITHUB_TOKEN do not start new workflow runs (only workflow_dispatch and repository_dispatch are exceptions). The Pages workflow's release: published trigger would never have fired in production since release-tag.yml's softprops/action-gh-release uses GITHUB_TOKEN to publish.

Fix: drop the release: published trigger and have release-tag.yml's release-github job explicitly run gh workflow run pages.yml --ref main after the release is created. release-github gains actions: write to authorize the dispatch.

Also adopts refinements from sibling org PRs (permission-pilot#356, bluemusic#220):

- Top-level permissions reduced to contents: read; pages: write and id-token: write moved to the deploy job only (least privilege)

- JEKYLL_GITHUB_TOKEN on the build step so jekyll-github-metadata authenticates when fetching site.github.releases

- Sanity-check step (test -f _site/index.html && _site/CNAME) fails fast if Jekyll produced nothing

- Explicit upload-pages-artifact path: ./_site matches the build's destination

- Verify fastlane Bundler wiring step (bundle exec fastlane --version) lets workflow_dispatch dry_run=true exercise the relocated Gemfile before the next real release
2026-04-28 12:08:23 +02:00
darken 96bc3f5df7 chore(ci): Move Gemfile to fastlane/ for tidier layout
The root Gemfile only ever declared the fastlane gem and lived next to fastlane configuration anyway. Moving it under fastlane/ matches that ownership and keeps the repo root cleaner.

release-gplay job now sets BUNDLE_GEMFILE=fastlane/Gemfile and runs ruby/setup-ruby with working-directory=fastlane so bundler-cache resolves the moved Gemfile. fastlane lanes still run from the repo root.
2026-04-28 12:08:23 +02:00
darken 1891a6d73a chore(ci): Migrate Pages to custom workflow with Node 24 actions
Replaces the auto pages-build-deployment (which still uses Node-20 actions/checkout@v4 and actions/upload-artifact@v4) with a custom workflow using configure-pages@v6, jekyll-build-pages@v1.0.13, upload-pages-artifact@v5, deploy-pages@v5.

Triggered by release publication so the changelog Liquid template (which reads site.github.releases) only rebuilds when a release actually exists. workflow_dispatch is kept for manual rebuilds when debugging Pages content.
2026-04-28 12:08:23 +02:00
darken 698415237d chore(ci): Drop unused Android build-cache step 2026-04-28 12:08:23 +02:00
darken 7ef672a11f chore(ci): Bump action-gh-release to v3.0.0 (Node 24) 2026-04-28 12:08:23 +02:00
darken f67e8aa7d1 fix: Harden foreground service exception handler 2026-04-25 23:40:10 +02:00
darken bcb4bdff08 fix(overview): Prevent crash from duplicate LazyList keys 2026-04-25 23:39:59 +02:00
darken 70d9432b2b Release: 5.1.1-rc0 2026-04-25 21:58:56 +02:00
darken 9d06839a1c General: Update app translations from Crowdin 2026-04-25 21:58:10 +02:00
darken 97f294e5f7 fix(widget): Persist theme settings across device reboot
Closes #549
2026-04-25 20:39:40 +02:00
darken a40599c1dc feat(settings): Tailor device settings to connection state
- Hide reactions and AAP sections unless device is classically connected

- Move advanced-settings-unavailable card to the bottom of the list

- Show 'device not nearby' infobox when out of range

- Show missing-paired-device banner with edit-profile action

- Replace pending banner with snackbar on user-initiated change
2026-04-25 19:00:19 +02:00
darken 175aef9e2f feat(logging): Log raw advertisement and decoded payload bytes 2026-04-25 16:19:21 +02:00
darken 36ae35918a refactor(widget): Move WidgetDeviceKey into widget package 2026-04-25 16:19:21 +02:00
darken 60ad1943b6 fix(widget): Gate refresh on display fields, drop log to VERBOSE 2026-04-25 16:19:21 +02:00
darken 5e727e907d refactor: Drop per-emission flow logger and redundant widget logs 2026-04-25 16:19:21 +02:00
darken 5360f54ea8 fix(privacy): Stop logging AirPods encryption keys 2026-04-25 16:19:21 +02:00
darken 486efe6e65 refactor: Reduce debug log noise from redundant state emissions 2026-04-25 16:19:21 +02:00
darken c2ff1f111e chore(reaction): Rename sleep notification channel label 2026-04-25 10:21:30 +02:00
darken 26b72726a7 feat(reaction): Pause media when AirPods detect sleep 2026-04-25 10:21:30 +02:00
darken f35fe968c6 docs(aap): Document SET_BAND_EDGES as RF band gating 2026-04-24 17:54:53 +02:00
darken 498f040a57 feat(aap): Drop experimental warning on Optimized Charge toggle
Verified on a real AirPods Pro 3: toggling flips pod charging state from CHARGING_OPTIMIZED to CHARGING and persists across reconnects. Apple-bool wire format is confirmed, so the 'experimental' warning box is no longer warranted.
2026-04-24 17:27:48 +02:00
darken 59b3106c3b feat(aap): Surface Optimized Charge indicator and Pro 3 toggle
Adds a per-battery 'Optimized' chip on the overview card when pods report wire value 0x05 (CHARGING_OPTIMIZED), which was already decoded but collapsed into a plain 'Charging' in the UI. On AirPods Pro 3, also adds a user-facing toggle for the device-side Optimized Charge Limit (AAP setting 0x3B).

- Decode setting 0x3B via decodeAppleBool so unknown values fall through instead of coercing to false

- Bypass ear-detection queue for SetDynamicEndOfCharge so the toggle works while pods sit in the closed case

- Expose per-slot ChargingState? on PodDevice; StatusChipRow renders 'Optimized' for CHARGING_OPTIMIZED, 'Charging' for CHARGING

- New BatteryCard in device settings with experimental warning (pattern matches Sleep Detection)

- Generic settingRejectedEvents flow alongside the existing offRejectedEvents so the toggle can show a dedicated snackbar on verification failure
2026-04-24 17:27:48 +02:00
darken 89e8b83be4 docs(aap): Revise PME hypothesis to Personal Medical Equipment 2026-04-24 15:27:18 +02:00
darken f0531796dd feat(device-settings): Surface hardware, pending firmware, and bonded dates 2026-04-24 13:08:57 +02:00
darken a701fdfba7 feat(aap): Build a comprehensive AAP protocol catalog
Adopts the Wireshark AAP dissector (pabloaul/apple-wireshark) as a third reference source alongside LibrePods and MagicPodsCore. Catalogues every known message type and control/setting ID, corrects DeviceInfo field labels, and adds sealed AapPacket hierarchy with Connect Response parsing. Case Info probe (Pro 3), Sleep event, and Dynamic End of Charge decoders are in place for future use.
2026-04-24 08:39:46 +02:00
darken 118eaa8d08 feat(widget): Add 1x1 size support to battery widget
Introduce BatteryLayout enum with size-driven dispatch. Drops minWidth from 80dp to 40dp so the battery widget can be placed at one cell. At 1-cell-wide placements render a compact icon+percent stack; wider sizes keep the current NARROW/WIDE layouts.
2026-04-23 16:11:07 +02:00
darken 016914ec05 fix(overview): Replace red triangle with inline missing-pairing banner 2026-04-23 14:17:16 +02:00
darken 0aadec0b75 feat(device-settings): Link AAP-unavailable card to compatibility tracker 2026-04-23 12:53:24 +02:00
darken e6cf507937 chore(claude): Refresh .claude/ docs to match current architecture
Drops stale app-common/Wear OS references, renames migrated classes

(ReactionSettingsFragment/PopUpPodViewFactory/PodMonitor), documents

the BLE/AAP split in monitor/core, the layered AAP stack, Widget

(Glance) and Upgrade subsystems, Navigation3 + legacy helpers. Fixes

localization examples and screenshot pipeline counts.
2026-04-23 08:58:27 +02:00
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
1180 changed files with 56478 additions and 8707 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 KiB

+2 -1
View File
@@ -1 +1,2 @@
worktrees/
worktrees/
scheduled_tasks.lock
+5 -8
View File
@@ -4,10 +4,7 @@ Android app that detects and monitors AirPods via Bluetooth LE. Displays battery
## Project Structure
| Module | Description |
|--------|-------------|
| `app/` | Main Android app (FOSS and Google Play flavors) |
| `app-common/` | Shared code between phone and Wear OS apps |
Single Gradle module `app/` with multiple source sets (`main`, `foss`, `gplay`, `debug`, `test`, `testFoss`, `testGplay`, `screenshotTest`). A previous `app-common/` module was merged into `app/`.
## Build Flavors
@@ -20,10 +17,10 @@ Quick build check: `./gradlew assembleFossDebug`
| Path | Contains |
|------|----------|
| `app/src/main/java/` | Main app source (activities, fragments, services) |
| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) |
| `app/src/main/java/` | Main app source (Compose screens, services, receivers, monitor, bluetooth, models) |
| `app/src/foss/java/`, `app/src/gplay/java/` | Flavor-specific code (e.g. upgrade/billing) |
| `app/src/main/res/` | Layouts, drawables, strings |
| `app-common/src/test/` | Unit tests |
| `app/src/test/`, `app/src/testFoss/`, `app/src/testGplay/` | Unit tests (shared + flavor-specific) |
| `app/build.gradle.kts` | App build config, dependencies, flavors |
| `app/src/debug/java/.../screenshots/` | Play Store screenshot content composables |
| `fastlane/` | Screenshot generation scripts, Play Store metadata |
@@ -31,7 +28,6 @@ Quick build check: `./gradlew assembleFossDebug`
## Development Tips
- Use `assembleFossDebug` as the fastest build variant for iteration
- Shared code goes in `app-common/`, app-specific code in `app/`
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
- Always use string resources for user-facing text (see localization rules)
- Check `git log --oneline -20` for commit message style before committing
@@ -47,3 +43,4 @@ Detailed guidelines are in `.claude/rules/`:
- `pull-requests.md` — PR title and description conventions
- `agent-instructions.md` — Sub-agent delegation and critical thinking
- `screenshots.md` — Play Store screenshot pipeline, commands, adding new screens
- `release.md` — Release workflow (`Release prepare` dispatch), inputs, channel mapping, rollback
+70 -31
View File
@@ -2,16 +2,22 @@
description: Architecture overview, module structure, key components, data flow, and dependencies
globs:
- "app/**/*.kt"
- "app-common/**/*.kt"
- "**/*.gradle.kts"
---
# Architecture
## Multi-Module Structure
## Single-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app and Wear OS app
One Gradle module: `app/`. Source sets:
- `main` — shared code (Compose UI, services, monitor, bluetooth, AAP protocol, widgets)
- `foss` / `gplay` — flavor-specific code (e.g. upgrade/billing implementations)
- `debug` — debug-only code including screenshot content composables
- `test` / `testFoss` / `testGplay` — unit tests
- `screenshotTest` — Compose Preview Screenshot tests for Play Store assets
A previous `app-common/` module was merged into `app/` (commit `be8f4919`).
## Core Patterns
@@ -22,18 +28,62 @@ globs:
## Key Components
### PodMonitor System
### Device Monitoring
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
- `MonitorControl`: Manages MonitorService lifecycle
- `MonitorService`: Foreground service that continuously scans for AirPods
- `BluetoothEventReceiver`: Handles system Bluetooth events
`monitor/core/` is split into two data-source siblings that `DeviceMonitor` merges:
- `monitor/core/ble/BlePodMonitor` — passive BLE scanning; reads Apple advertisement beacons (battery, case state, in-ear, etc.). Works for any pod in range; no pairing required
- `monitor/core/aap/` — AAP connection lifecycle layer on top of `AapConnectionManager`:
- `AapLifecycleManager` — starts/stops the AAP subsystem
- `AapAutoConnect` — auto-opens AAP sessions for bonded/known devices
- `AapKeyPersister`, `AapLearnedSettingsPersister` — persist session keys and learned pod settings across app restarts
- `StemConfigSender`, `StemPressReaction`, `AncGestureResolver` — push config and react to stem/HID events
- `monitor/core/cache/DeviceStateCache` — persisted last-known state so profiles still show data when a device is out of range
- `DeviceMonitor` — singleton that `combine`s `BlePodMonitor.devices + AapConnectionManager.allStates + DeviceStateCache + profiles` into unified `PodDevice` objects. ViewModels observe `DeviceMonitor.devices`; they do **not** reach into `BlePodMonitor` or the AAP layer directly
- `MonitorControl` / `MonitorService` — foreground service lifecycle holding the scan awake
- `BluetoothEventReceiver`, `BootCompletedReceiver` — system triggers that wake the service
**BLE vs AAP — what each path gives you:**
| | BLE (advertisements) | AAP (L2CAP session) |
|---|---|---|
| Direction | Read-only, passive | Bidirectional commands + events |
| Prerequisite | Bluetooth on | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
| Data | Battery, case open, in-ear, pod model | Settings, ANC mode control, press controls, stem events, device info |
| Availability | Any pod in range | Only your own paired pods |
### Reaction System
- `ReactionSettingsFragment`: Configuration for popup notifications
- `ReactionsCard`: Compose UI for reaction settings, embedded in the device settings screen
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpPodViewFactory`: Creates UI components for different pod models
- `PopUpContent`: Compose pod rendering — model-specific UI branches inline, no factory class
### Widget System (Glance)
- `BatteryGlanceWidget`, `AncGlanceWidget`: Jetpack Glance-based home-screen widgets
- `WidgetConfigurationActivity`: Configuration UI launched on widget placement
- Lives under `app/src/main/java/eu/darken/capod/main/ui/widget/`
### Upgrade / Pro Features
- `UpgradeRepo` interface with two flavor implementations:
- `UpgradeRepoGplay` — billing-client backed, includes grace-period handling for interrupted purchases
- `UpgradeControlFoss` — cache/sponsor-backed; users are `isPro = false` until they call `upgrade()`, after which the pro flag is persisted via DataStore
- FOSS is **not** "always pro" — it's opt-in via a local sponsor flow
### AAP (Apple Accessory Protocol) Stack
Three-layer structure under `pods/core/apple/aap/`:
- **`protocol/`** — pure data: `AapMessage`, `AapCommand`, `AapSetting`, `AapDeviceProfile`, `AapDeviceInfo`, `StemPressEvent`, `KeyExchangeResult`. Plus `DefaultAapDeviceProfile` and `Model.Features` capturing per-model capability
- **`engine/`** — session state machine for one connection:
- `AapConnection` — the L2CAP socket wrapper
- `AapSessionEngine` — drives the session lifecycle; tested in `AapSessionEngineTest`
- `AapInboundInterpreter` / `AapOutboundController` — decode incoming messages, encode outgoing
- `AapSettingsCoordinator`, `AapAncController`, `HidTracker`, `AapDeviceInfoDiagnostics` — feature-specific coordinators that sit on top of the session
- **`AapConnectionManager`** (singleton) — owns all open AAP sessions keyed by `BluetoothAddress`, uses `L2capSocketFactory` to create sockets. Consumers don't touch `AapConnection` directly — they call `sendCommand(...)` and observe `allStates`
The monitor-layer glue (`monitor/core/aap/`) described above wires this stack into the foreground service and persists its learned state.
### Common Utilities
@@ -54,31 +104,20 @@ globs:
## Data Flow
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorService` scans for AirPods beacon data
3. `PodMonitor` processes and stores device information
4. ViewModels observe monitor data via repositories
5. UI components react to ViewModel state changes
6. `ReactionSystem` triggers popups and notifications
## Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorService` which runs as a foreground service.
## Multi-Platform Considerations
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms.
1. `BluetoothEventReceiver` / `BootCompletedReceiver` wake `MonitorService` (foreground)
2. `MonitorService` keeps `BlePodMonitor` scanning (passive advertisements) and `AapLifecycleManager` running (active L2CAP sessions via `AapConnectionManager`)
3. `DeviceMonitor` merges BLE + AAP + cached state + profiles into `PodDevice` objects
4. ViewModels (`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel`, widget view models) observe `DeviceMonitor.devices`; settings/command changes are sent back through `AapConnectionManager.sendCommand(...)`
5. Reaction triggers (case-open popup, auto-play, notifications) and widget state updates react to the merged flow
## Testing Strategy
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
- **Unit Tests**: `app/src/test/` (shared), `app/src/testFoss/`, `app/src/testGplay/` (flavor-specific — e.g. `UpgradeRepoGplayTest`, `FossUpgradeSerializationTest`)
- **Screenshot Tests**: `app/src/screenshotTest/` — Compose Preview Screenshot Testing, powers the Play Store screenshot pipeline
## Key Dependencies
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **Navigation**: Navigation3 (`addNavigation3()`) drives current Compose screen routing. Some legacy `androidx.navigation` helpers still exist (`NavDirectionsExtensions`, `ViewModel3`) — don't assume SafeArgs is fully gone
- **kotlinx.serialization**: JSON serialization for configuration and caching
- **Material Design**: UI components following Material Design guidelines
- **Material Design 3**: Compose Material3 UI components
+1
View File
@@ -21,6 +21,7 @@ Use the existing commit history as reference. Common prefixes:
- **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`)
- **feat**: New features
- **refactor**: Code restructuring without behavior change
- **ui**: Visual/layout-only tweaks that aren't a full feature or refactor (e.g., `ui(overview): ...`)
- **chore**: Maintenance, dependency updates, build config
- **docs**: Documentation changes
+5 -3
View File
@@ -9,7 +9,7 @@ globs:
When adding new user-facing strings:
- **Always use string resources**: Never hardcode user-facing text in layouts or code
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_monitor_mode_label`)
- **Provide context**: String names should indicate usage and location
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
@@ -17,5 +17,7 @@ When adding new user-facing strings:
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)
- `profiles_name_default` (default value)
- `troubleshooter_ble_result_failure_title` (status/error title)
Common prefixes currently in use: `device_`, `settings_`, `support_`, `profiles_`, `press_`, `general_`, `pods_`, `upgrade_`, `widget_`, `debug_`, `permission_`, `troubleshooter_`, `overview_`, `anc_`, `onboarding_`. There is no `error_*` prefix — error labels live under the relevant feature (e.g. `general_error_label`, `troubleshooter_*_failure_*`).
+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
+78
View File
@@ -0,0 +1,78 @@
# Release Process
Releases are cut via the **Release prepare** workflow (`.github/workflows/release-prepare.yml`). It bumps `version.properties` and `VERSION`, commits to `main`, tags `v<version>`, pushes atomically, and dispatches `release-tag.yml` which builds, signs, and uploads.
## Dispatch
```bash
# Plan only — no commit, no tag, no push.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=true
# Real cut.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=false
```
After `dry_run=false`: Job 1 computes + writes the summary, then Job 2 immediately commits/tags/pushes (no env gate — cancel the run between Job 1 and Job 2 if the summary looks wrong; you have ~seconds). The tag push naturally triggers `release-tag.yml` (the App-token push fires `on: push:` workflows; only `GITHUB_TOKEN`-pushes are suppressed). `release-tag.yml` then runs `validate-tag` and the existing `release-github` (`foss-production` approval) + `release-gplay` (`gplay-production` approval) jobs — those are the two human checkpoints, matching the pre-migration UX.
## Inputs
| Input | Default | Notes |
|---|---|---|
| `bump_kind` | `build` | `build` \| `patch` \| `minor` \| `major` |
| `version_type` | `keep-current` | Preserves current `rc`/`beta`. Set explicitly to switch. |
| `version_override` | empty | e.g. `5.1.2-rc0`. Bypasses bump_kind/version_type. |
| `expected_current` | empty | Optional: fail if `version.properties` ≠ this. Useful for tight coordination. |
| `dry_run` | `true` | Default is plan-only. |
Bump rules: `build` increments build; `patch`/`minor`/`major` zero everything to the right of the bumped field. All numeric fields bounded `0..99` (the `versionCode` formula collapses at ≥100).
## Local
```bash
./tools/release/bump.sh --mode=plan --bump-kind=build --version-type=keep-current
./tools/release/bump.sh --mode=check
bats tools/release/bump.bats
```
## Channel mapping
| Tag suffix | FOSS APK | GitHub release | Fastlane lane | Play track | Rollout |
|---|---|---|---|---|---|
| `-beta*` | `assembleFossBeta` | pre-release | `beta` | `beta` | 10% |
| `-rc*` (or anything else) | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
`lane :production` in `Fastfile` uploads to Play's **beta** track at 10% — manually promoted to production via Play Console.
## Rollback
| Stage reached | Steps |
|---|---|
| Bump on `main`, downstream not started | `git push origin :refs/tags/v<bad>`, `git revert <bump-sha>`, push |
| GitHub release created | Above + `gh release delete v<bad> --yes --cleanup-tag` |
| Play upload completed | Above + halt rollout in Play Console (or `bundle exec fastlane supply --track beta --rollout 0 --version-code <bad-code>`) |
| Job 2 ran but downstream rejected at env approval | Treat as first row — bump+tag are public on `main` regardless of downstream outcome |
`bump.sh` enforces strict `versionCode` monotonicity, so re-using a code is impossible without manually editing `version.properties`.
## Auth setup
`release-prepare.yml` Job 2 uses a GitHub App token (not `GITHUB_TOKEN`) to push the bump commit and tag. The App identity is in the rulesets' bypass list, which is what allows the push to bypass branch protection + tag-creation restrictions.
Required org secrets (set on the d4rken-org organization, accessible to `capod`):
- `RELEASE_APP_CLIENT_ID` — Client ID of the `d4rken-org-releaser` GitHub App (visible on the App's settings page, format `Iv1.<hex>` or similar)
- `RELEASE_APP_PRIVATE_KEY` — full `.pem` contents (including BEGIN/END lines)
The App is installed on this repo and added as a bypass actor to:
- The main-branch ruleset (PR + status check requirements)
- The tag ruleset (creation restriction on `v*`)
Other apps in the org can reuse the same App + secrets — just install the App on each repo and add it to that repo's rulesets' bypass lists.
## Defense in depth
`release-tag.yml` includes `validate-tag` which: (1) regex-checks `github.ref_name`, (2) runs `bump.sh --mode=check`, (3) asserts the parsed name matches the tag. Manual `gh workflow run release-tag.yml --ref vfoo` or hand-pushed tags fail before any build.
## Stuck-dispatch recovery
If Job 2's atomic push lands but the natural `on: push:` trigger doesn't fire `release-tag.yml` (rare — would mean GitHub dropped the event), the tag is public but no pipeline runs. Re-dispatch manually: `gh workflow run release-tag.yml --ref v<new> -f dry_run=false`.
+39 -12
View File
@@ -19,25 +19,33 @@ ScreenshotContent.kt (mock data + composables)
| File | Purpose |
|------|---------|
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (8 screens) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions that wire content to locale annotations |
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (7 screens) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions (currently: `DashboardLight`, `DashboardDark`, `CasePopUp`, `DeviceProfiles`, `AddProfile`, `DeviceSettingsReactions`, `WidgetConfiguration`) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) |
| `fastlane/generate_screenshots.sh` | Batched generation across 68 locales |
| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script |
| `fastlane/copy_screenshots.sh` | Copies rendered PNGs into fastlane structure |
## Commit policy
Only the 6 smoke locales (en-US, de-DE, ja-JP, ar, zh-CN, pt-BR) have `phoneScreenshots/*.png` checked into the repo. Non-smoke locales are excluded by `.gitignore`. This mirrors permission-pilot and keeps repo size small (~7 MB vs ~67 MB for the full 68 locales).
Play Store's `supply` only uploads what's present in `fastlane/metadata/android/<locale>/images/phoneScreenshots/`. For locales not in the upload, Play Store retains whatever was last pushed. So full localization on Play Store is maintained by an **occasional manual** full regen + `:screenshots_only` upload — not by every PR.
## Commands
```bash
# Full run — all 68 locales, ~12 batches, ~7 minutes
./fastlane/generate_screenshots.sh
# Smoke test — 6 locales (en, de, ja, ar, zh-CN, pt-BR), single batch
# Default — smoke set (6 locales × 7 screens, ~42 PNGs, single batch).
# Use this for local iteration and PRs that touch screenshot content.
./fastlane/generate_screenshots.sh --smoke
# Full run — all 68 locales. Use only when intending to upload to Play Store
# (the non-smoke output is .gitignored and should not be committed).
./fastlane/generate_screenshots.sh
# Copy into fastlane directories (run after generate)
./fastlane/copy_screenshots.sh
# Clean copy (removes old screenshots first)
# Clean copy (removes old screenshots first) — REQUIRED when screens are removed or renamed
./fastlane/copy_screenshots.sh --clean
```
@@ -47,20 +55,39 @@ ScreenshotContent.kt (mock data + composables)
2. Add a `@PreviewTest` function in `PlayStoreScreenshots.kt` that calls it
3. Add the function name → filename mapping in `copy_screenshots.sh` `SCREEN_MAP`
4. Update the expected count in `generate_screenshots.sh` (composables per locale)
5. Run the full pipeline: `generate_screenshots.sh` then `copy_screenshots.sh`
5. Run the smoke pipeline: `generate_screenshots.sh --smoke` then `copy_screenshots.sh --clean`
## Removing or Renaming a Screenshot
1. Remove the `@PreviewTest` entry and its `SCREEN_MAP` mapping
2. Run `generate_screenshots.sh --smoke`
3. Run `copy_screenshots.sh --clean`**`--clean` is required** here; without it, old files (e.g. a renamed `8_reaction_settings.png`) stay in `fastlane/metadata/android/<smoke locale>/images/phoneScreenshots/` and get uploaded to Play Store
## After UI Changes
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate:
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate the smoke set:
```bash
./fastlane/generate_screenshots.sh
./fastlane/generate_screenshots.sh --smoke
./fastlane/copy_screenshots.sh --clean
```
## Refreshing all locales on Play Store
Periodic, manual operation — not per-PR:
```bash
./fastlane/generate_screenshots.sh # full, ~30 min, 477 PNGs
./fastlane/copy_screenshots.sh --clean
bundle exec fastlane screenshots_only # uploads all 68 locales to Play Store
git checkout -- fastlane/metadata/android/ # discard non-smoke changes (gitignored anyway)
```
The `.gitignore` rule keeps non-smoke output unstaged automatically, so only the smoke locales' refreshed PNGs would show up as modifications and can be committed.
## Technical Notes
- Batch size defaults to 2 locales (16 renders) to avoid layoutlib memory leak (~10MB/image)
- Batch size defaults to 2 locales; renders per batch = `BATCH_SIZE × screen count` (currently 2 × 7 = 14). Small batches avoid layoutlib memory leaks (~10MB/image)
- Gradle daemon is stopped between batches to release memory
- `PlayStoreLocales.kt` is temporarily rewritten per batch and restored via trap
- Device spec: 1080x2400px @ 428 DPI (Pixel-class phone)
+1 -10
View File
@@ -30,13 +30,4 @@ runs:
~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'buildSrc/**/*.kt') }}
restore-keys: |
${{ runner.os }}-gradle-caches-
- name: Cache Android Global Build-Cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
with:
path: |
~/.android/build-cache
key: ${{ runner.os }}-android-build-cache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-android-build-cache-
${{ runner.os }}-gradle-caches-
+27 -1
View File
@@ -3,6 +3,9 @@ name: Code tests & eval
on:
push:
branches: [ main ]
paths-ignore:
- VERSION
- version.properties
pull_request:
branches: [ main ]
@@ -78,4 +81,27 @@ jobs:
with:
persist-credentials: false
- name: Validate metadata lengths
run: bash fastlane/check_metadata_length.sh
run: bash fastlane/check_metadata_length.sh
check-release-tooling:
name: Release tooling
runs-on: ubuntu-22.04
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Install bats and shellcheck
run: |
sudo apt-get update
sudo apt-get install -y bats shellcheck
- name: Shellcheck bump.sh
run: shellcheck tools/release/bump.sh
- name: Run bump.sh unit tests
run: bats tools/release/bump.bats
- name: Verify version.properties + VERSION are consistent
run: ./tools/release/bump.sh --mode=check
@@ -4,6 +4,9 @@ on:
push:
branches:
- main
paths-ignore:
- VERSION
- version.properties
pull_request:
branches:
- main
+66
View File
@@ -0,0 +1,66 @@
name: Deploy GitHub Pages
on:
push:
branches: [ main ]
paths:
- _config.yml
- _layouts/**
- README.md
- CHANGELOG.md
- PRIVACY_POLICY.md
- CNAME
- .github/workflows/pages.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Jekyll site
runs-on: ubuntu-22.04
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Configure Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d #v6.0.0
- name: Build with Jekyll
uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 #v1.0.13
with:
source: ./
destination: ./_site
env:
JEKYLL_GITHUB_TOKEN: ${{ github.token }}
- name: Sanity-check Jekyll output
run: test -f _site/index.html && test -f _site/CNAME
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 #v5.0.0
with:
path: ./_site
deploy:
name: Deploy to GitHub Pages
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 #v5.0.0
+235
View File
@@ -0,0 +1,235 @@
name: Release prepare
on:
workflow_dispatch:
inputs:
bump_kind:
description: 'How to bump the version (ignored if version_override is set)'
type: choice
options: [build, patch, minor, major]
default: build
version_type:
description: 'Channel for the new version (keep-current preserves current type)'
type: choice
options: [keep-current, rc, beta]
default: keep-current
version_override:
description: 'Explicit version, e.g. 5.1.2-rc0 (overrides bump_kind/version_type)'
type: string
default: ''
expected_current:
description: 'Optional safety check: fail if current version.properties does not match (e.g. 5.1.1-rc0)'
type: string
default: ''
dry_run:
description: 'When true: compute and validate only. When false: commit, tag, push, dispatch release-tag.yml.'
type: boolean
default: true
permissions:
contents: read
concurrency:
group: release-prepare-main
cancel-in-progress: false
jobs:
compute-and-validate:
name: Compute and validate
runs-on: ubuntu-22.04
permissions:
contents: read
outputs:
new_name: ${{ steps.plan.outputs.new_name }}
new_code: ${{ steps.plan.outputs.new_code }}
current_name: ${{ steps.plan.outputs.current_name }}
env:
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
INPUT_VERSION_TYPE: ${{ inputs.version_type }}
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
INPUT_EXPECTED_CURRENT: ${{ inputs.expected_current }}
steps:
- name: Guard ref must be main
run: |
if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then
echo "Must dispatch from main, got ${GITHUB_REF}" >&2
exit 1
fi
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Compute and validate
id: plan
run: |
set -euo pipefail
args=("--mode=plan")
if [[ -n "${INPUT_VERSION_OVERRIDE}" ]]; then
args+=("--version-override=${INPUT_VERSION_OVERRIDE}")
else
args+=("--bump-kind=${INPUT_BUMP_KIND}")
args+=("--version-type=${INPUT_VERSION_TYPE}")
fi
if [[ -n "${INPUT_EXPECTED_CURRENT}" ]]; then
args+=("--expected-current=${INPUT_EXPECTED_CURRENT}")
fi
./tools/release/bump.sh "${args[@]}" | tee plan.txt
{
grep -E '^current_name=' plan.txt
grep -E '^new_name=' plan.txt
grep -E '^new_code=' plan.txt
} >> "$GITHUB_OUTPUT"
- name: Tag collision check (local + remote)
env:
NEW_NAME: ${{ steps.plan.outputs.new_name }}
run: |
set -euo pipefail
if git rev-parse --verify "refs/tags/v${NEW_NAME}" >/dev/null 2>&1; then
echo "Local tag v${NEW_NAME} already exists" >&2
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW_NAME}" >/dev/null; then
echo "Remote tag v${NEW_NAME} already exists" >&2
exit 1
fi
- name: Write step summary
env:
CURRENT_NAME: ${{ steps.plan.outputs.current_name }}
NEW_NAME: ${{ steps.plan.outputs.new_name }}
NEW_CODE: ${{ steps.plan.outputs.new_code }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
{
echo "## Release plan"
echo
echo "| | |"
echo "|---|---|"
echo "| Current | \`${CURRENT_NAME}\` |"
echo "| New | \`${NEW_NAME}\` (code ${NEW_CODE}) |"
echo "| Tag | \`v${NEW_NAME}\` |"
echo "| Dry run | \`${DRY_RUN}\` |"
echo
echo "### bump.sh output"
echo
echo '```'
cat plan.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
push-and-dispatch:
name: Push and dispatch
needs: compute-and-validate
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-22.04
permissions:
contents: read
env:
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
INPUT_VERSION_TYPE: ${{ inputs.version_type }}
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
NEW_NAME: ${{ needs.compute-and-validate.outputs.new_name }}
NEW_CODE: ${{ needs.compute-and-validate.outputs.new_code }}
CURRENT_NAME_AT_PLAN: ${{ needs.compute-and-validate.outputs.current_name }}
steps:
- name: Mint App token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 #v3.1.1
with:
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Resolve bot identity
id: bot
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
set -euo pipefail
user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)
echo "user_name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
echo "user_email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"
- name: Checkout main with credentials
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: true
token: ${{ steps.app-token.outputs.token }}
- name: Verify state still matches plan from Job 1
run: |
set -euo pipefail
./tools/release/bump.sh --mode=check --expected-current="${CURRENT_NAME_AT_PLAN}"
- name: Re-check tag collision (state may have moved between jobs)
run: |
set -euo pipefail
if git rev-parse --verify "refs/tags/v${NEW_NAME}" >/dev/null 2>&1; then
echo "Local tag v${NEW_NAME} already exists" >&2
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW_NAME}" >/dev/null; then
echo "Remote tag v${NEW_NAME} already exists" >&2
exit 1
fi
- name: Apply bump
run: |
set -euo pipefail
args=("--mode=write" "--expected-current=${CURRENT_NAME_AT_PLAN}")
if [[ -n "${INPUT_VERSION_OVERRIDE}" ]]; then
args+=("--version-override=${INPUT_VERSION_OVERRIDE}")
else
args+=("--bump-kind=${INPUT_BUMP_KIND}")
args+=("--version-type=${INPUT_VERSION_TYPE}")
fi
./tools/release/bump.sh "${args[@]}"
- name: Verify post-write state matches plan
run: |
set -euo pipefail
./tools/release/bump.sh --mode=check --expected-current="${NEW_NAME}"
- name: Configure git identity
env:
BOT_USER_NAME: ${{ steps.bot.outputs.user_name }}
BOT_USER_EMAIL: ${{ steps.bot.outputs.user_email }}
run: |
git config user.name "${BOT_USER_NAME}"
git config user.email "${BOT_USER_EMAIL}"
- name: Commit and tag
run: |
set -euo pipefail
git add version.properties VERSION
git commit -m "Release: ${NEW_NAME}"
git tag -a "v${NEW_NAME}" -m "Release v${NEW_NAME}"
- name: Atomic push (commit + tag)
run: |
set -euo pipefail
git push --atomic origin "HEAD:refs/heads/main" "refs/tags/v${NEW_NAME}"
- name: Write step summary
run: |
set -euo pipefail
{
echo "## Released"
echo
echo "| | |"
echo "|---|---|"
echo "| Tag | \`v${NEW_NAME}\` |"
echo "| Version code | \`${NEW_CODE}\` |"
echo "| Bump commit | on \`main\` |"
echo "| Downstream | dispatched \`release-tag.yml\` |"
echo
echo "Watch the [Tagged releases](../../actions/workflows/release-tag.yml) workflow for the build + upload."
} >> "$GITHUB_STEP_SUMMARY"
+58 -2
View File
@@ -14,11 +14,54 @@ on:
permissions:
contents: read
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
validate-tag:
name: Validate tag
runs-on: ubuntu-22.04
steps:
- name: Check tag-name format
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ ! "${REF_NAME}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}-(rc|beta)[0-9]{1,2}$ ]]; then
echo "Tag '${REF_NAME}' does not match v<M.m.p-(rc|beta)n>" >&2
echo "Releases must be cut via the 'Release prepare' workflow." >&2
exit 1
fi
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 1
persist-credentials: false
- name: Verify version.properties matches tag
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# Strip leading 'v' to get the bare version name.
tag_name="${REF_NAME#v}"
# bump.sh in check mode emits current_name=...
parsed=$(./tools/release/bump.sh --mode=check)
current_name=$(echo "$parsed" | grep -E '^current_name=' | cut -d= -f2)
if [[ "$current_name" != "$tag_name" ]]; then
echo "Tag '${REF_NAME}' does not match version.properties name '$current_name'" >&2
echo "version.properties + VERSION must equal the tag — releases must be cut via 'Release prepare'." >&2
exit 1
fi
release-github:
needs: validate-tag
name: Create GitHub release
permissions:
contents: write
actions: write
runs-on: ubuntu-22.04
environment: foss-production
steps:
@@ -61,7 +104,7 @@ jobs:
- name: Create pre-release
if: contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b #v2.5.0
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda #v3.0.0
with:
prerelease: true
tag_name: ${{ github.ref_name }}
@@ -73,7 +116,7 @@ jobs:
- name: Create release
if: "!contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)"
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b #v2.5.0
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda #v3.0.0
with:
prerelease: false
tag_name: ${{ github.ref_name }}
@@ -83,10 +126,19 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger GitHub Pages deployment
if: "!(github.event_name == 'workflow_dispatch' && inputs.dry_run)"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run pages.yml --ref main
release-gplay:
needs: validate-tag
name: Create Google Play release
runs-on: ubuntu-22.04
environment: gplay-production
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/fastlane/Gemfile
steps:
- name: Decode Keystore
env:
@@ -122,6 +174,10 @@ jobs:
with:
ruby-version: 3.3.6
bundler-cache: true
working-directory: fastlane
- name: Verify fastlane Bundler wiring
run: bundle exec fastlane --version
- name: Assemble beta and upload to Google Play
if: contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
+16 -1
View File
@@ -16,4 +16,19 @@
/fastlane/README.md
.kotlin
# Screenshot test reference images (ephemeral, regenerated on demand)
app/src/screenshotTest*/reference/
app/src/screenshotTest*/reference/
# Play Store phone screenshots: commit only the 6 smoke locales (mirrors permission-pilot).
# Full localization is uploaded by occasional manual regen + screenshots_only;
# Play Store retains previously-uploaded screenshots for locales not pushed.
fastlane/metadata/android/*/images/phoneScreenshots/*.png
!fastlane/metadata/android/en-US/images/phoneScreenshots/*.png
!fastlane/metadata/android/de-DE/images/phoneScreenshots/*.png
!fastlane/metadata/android/ja-JP/images/phoneScreenshots/*.png
!fastlane/metadata/android/ar/images/phoneScreenshots/*.png
!fastlane/metadata/android/zh-CN/images/phoneScreenshots/*.png
!fastlane/metadata/android/pt-BR/images/phoneScreenshots/*.png
.codex
protocol-research/
_site/
.jekyll-cache/
vendor/bundle/
+12 -3
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
@@ -72,8 +81,7 @@ Currently supported models:
## Screenshots
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
<img src="https://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
<img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1_dashboard_light.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2_dashboard_dark.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/3_case_popup.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/4_widget_configuration.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5_device_profiles.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/6_add_profile.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/7_device_settings_reactions.png" width="100">
## Thanks to
@@ -87,6 +95,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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

+1 -1
View File
@@ -1 +1 @@
5.0.0-beta0 50000000
5.2.1-rc0 50201000
-3
View File
@@ -20,9 +20,6 @@ exclude:
- buildSrc
- gradle/wrapper
- fastlane
- Gemfile
- Gemfile.lock
- crowdin*
- app
- app-common
- CONTRIBUTING.md
+1
View File
@@ -130,6 +130,7 @@ android {
//noinspection WrongGradleMethod
tasks.withType<Test> {
useJUnitPlatform()
maxHeapSize = "4g"
}
}
}
@@ -14,43 +14,76 @@ 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 eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.compose.PreviewWrapper
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.monitor.core.battery.BatteryEstimate
import androidx.compose.ui.platform.LocalContext
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.main.ui.widget.withLocalizedPreviewEstimates
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"
@Composable
internal fun DashboardContent() = PreviewWrapper {
internal fun DashboardContent(showAap: Boolean = false) = PreviewWrapper {
val devices = if (showAap) {
listOf(
MockPodDataProvider.dualPodMonitoredWithAap(),
MockPodDataProvider.singlePodMonitoredWithAap(),
MockPodDataProvider.unknownMonitored(),
)
} else {
listOf(
MockPodDataProvider.dualPodMonitoredMixed(),
MockPodDataProvider.singlePodMonitored(),
MockPodDataProvider.unknownMonitored(),
)
}
// Plausible estimates for the mock battery levels: runtime under each gauge, charge ETA in
// the charging chip of whichever mock pod is charging.
val estimates = if (showAap) {
mapOf(
"preview-dual-aap" to BatteryEstimate(
left = BatteryEstimate.Pod(285, 0.17f, BatteryEstimate.Source.LIVE),
right = BatteryEstimate.Pod(160, 0.17f, BatteryEstimate.Source.LIVE),
),
"preview-single-aap" to BatteryEstimate(
headset = BatteryEstimate.Pod(480, 0.05f, BatteryEstimate.Source.LIVE, minutesUntilCharged = 45),
),
)
} else {
mapOf(
"preview-dual-mixed" to BatteryEstimate(
left = BatteryEstimate.Pod(285, 0.17f, BatteryEstimate.Source.LIVE, minutesUntilCharged = 20),
right = BatteryEstimate.Pod(160, 0.17f, BatteryEstimate.Source.LIVE),
),
"preview-single" to BatteryEstimate(
headset = BatteryEstimate.Pod(1020, 0.05f, BatteryEstimate.Source.LIVE),
),
)
}
OverviewScreen(
state = OverviewViewModel.State(
now = MOCK_NOW,
permissions = emptySet(),
devices = listOf(
MockPodDataProvider.dualPodMonitoredMixed(),
MockPodDataProvider.singlePodMonitored(),
MockPodDataProvider.unknownMonitored(),
),
isDebugMode = false,
devices = devices,
isDebug = false,
isBluetoothEnabled = true,
profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodModel.AIRPODS_PRO2),
@@ -58,8 +91,10 @@ internal fun DashboardContent() = PreviewWrapper {
),
upgradeInfo = MockPodDataProvider.gplayInfo(isPro = true),
showUnmatchedDevices = false,
batteryEstimates = estimates,
),
onRequestPermission = {},
onBluetoothSettings = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
@@ -85,19 +120,38 @@ internal fun DeviceProfilesContent() = PreviewWrapper {
@Composable
internal fun AddProfileContent() = PreviewWrapper {
val pairedAirPods = BluetoothDevice2(
address = "AA:BB:CC:DD:EE:FF",
name = "AirPods Pro",
seenFirstAt = MOCK_NOW,
)
val bondedItems = listOf(
DeviceProfileCreationViewModel.BondedDeviceItem(
device = pairedAirPods,
claimedByProfile = null,
),
DeviceProfileCreationViewModel.BondedDeviceItem(
device = BluetoothDevice2(
address = "11:22:33:44:55:66",
name = "Living Room TV",
seenFirstAt = MOCK_NOW,
),
claimedByProfile = null,
),
)
DeviceProfileCreationScreen(
state = DeviceProfileCreationViewModel.State(
isEditMode = false,
name = "",
name = "My AirPods Pro",
nameError = null,
selectedModel = null,
selectedModel = PodModel.AIRPODS_PRO2,
availableModels = PodModel.entries.filter { it != PodModel.UNKNOWN },
identityKey = null,
encryptionKey = null,
selectedDevice = null,
bondedDeviceItems = emptyList(),
selectedDevice = pairedAirPods,
bondedDeviceItems = bondedItems,
minimumSignalQuality = 0.15f,
canSave = false,
canSave = true,
),
onBack = {},
onSave = {},
@@ -113,44 +167,16 @@ 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,
isClassicallyConnected = true,
),
onNavigateUp = {},
onOnePodModeChanged = {},
onAutoPlayChanged = {},
onAutoPauseChanged = {},
onAutoConnectChanged = {},
onAutoConnectConditionSelected = {},
onShowPopUpOnCaseOpenChanged = {},
onShowPopUpOnConnectionChanged = {},
)
}
@@ -230,7 +256,7 @@ internal fun HomescreenWidgetContent() {
bgColor = MaterialTheme.colorScheme.surface.toArgb(),
textColor = MaterialTheme.colorScheme.onSurface.toArgb(),
iconColor = MaterialTheme.colorScheme.onSurface.toArgb(),
),
).withLocalizedPreviewEstimates(LocalContext.current),
)
}
}
@@ -242,7 +268,7 @@ private fun PreviewDashboardLight() = DashboardContent()
@Preview(name = "2 - Dashboard Dark", locale = "en", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true)
@Composable
private fun PreviewDashboardDark() = DashboardContent()
private fun PreviewDashboardDark() = DashboardContent(showAap = true)
@Preview(name = "3 - Case Pop-up", device = DS, showSystemUi = true)
@Composable
@@ -260,14 +286,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()
@@ -35,7 +35,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -109,7 +108,7 @@ 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),
@@ -120,143 +119,144 @@ fun UpgradeScreen(
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
TopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
)
}
},
)
},
) { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues)
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
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))
}
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,
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))
}
}
}
@@ -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>
+7 -6
View File
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">تبرُّع</string>
<string name="foss_upgrade_alreadydonated_label">أنا متبرع بالفعل</string>
<string name="foss_upgrade_no_money_label">أُنفق كل أموالي على AirPods</string>
<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="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_sponsor_action">دعم التطوير</string>
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبُّع. لا قيود من جوجل بلاي.</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">İ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>
+3 -2
View File
@@ -5,6 +5,7 @@
<string name="foss_upgrade_no_money_label">Es iztērēju visu savu naudu AirPods austiņām</string>
<string name="upgrade_foss_preamble">CAPod FOSS ir bezmaksas un atkļējkoda. Ja tā ir noderba, apsveriet attīstības sponsēšanu, lai palīdzētu turpināt projektu.</string>
<string name="upgrade_foss_sponsor_action">Sponsorēt attīstību</string>
<string name="upgrade_foss_sponsor_subtitle">Bez reklamām. Bez sekš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_foss_sponsor_subtitle">Bez reklamām. Bez izsekošanas. Bez Google Play saistībām.</string>
<string name="upgrade_foss_sponsor_returned_early">Jau atpakaļ? Tavs atbalsts uztur CAPod esamību.</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>

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