Compare commits

..
22 Commits
Author SHA1 Message Date
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
168 changed files with 5802 additions and 958 deletions
+4 -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
+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_*`).
+13 -6
View File
@@ -19,16 +19,17 @@ 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 |
## Commands
```bash
# Full run — all 68 locales, ~12 batches, ~7 minutes
# Full run — iterates over ALL_LOCALES in batches of BATCH_SIZE.
# The script prints "Locales: N | Batch size: B | Batches: ceil(N/B)" at startup.
./fastlane/generate_screenshots.sh
# Smoke test — 6 locales (en, de, ja, ar, zh-CN, pt-BR), single batch
@@ -37,7 +38,7 @@ ScreenshotContent.kt (mock data + composables)
# 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
```
@@ -49,6 +50,12 @@ ScreenshotContent.kt (mock data + composables)
4. Update the expected count in `generate_screenshots.sh` (composables per locale)
5. Run the full pipeline: `generate_screenshots.sh` then `copy_screenshots.sh`
## Removing or Renaming a Screenshot
1. Remove the `@PreviewTest` entry and its `SCREEN_MAP` mapping
2. Run `generate_screenshots.sh`
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/*/images/phoneScreenshots/` and get uploaded to Play Store
## After UI Changes
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate:
@@ -60,7 +67,7 @@ When modifying a screen that appears in screenshots (check `ScreenshotContent.kt
## 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 -1
View File
@@ -1 +1 @@
5.1.0-rc0 50100000
5.1.1-rc0 50101000
@@ -118,6 +118,7 @@ internal fun DeviceSettingsReactionsContent() = PreviewWrapper {
now = MOCK_NOW,
isPro = true,
isNudgeAvailable = true,
isClassicallyConnected = true,
),
onNavigateUp = {},
)
+6 -3
View File
@@ -10,21 +10,24 @@ import eu.darken.capod.common.debug.logging.LogCatLogger
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.ui.widget.WidgetManager
import eu.darken.capod.main.ui.widget.toWidgetKey
import eu.darken.capod.monitor.core.DeviceMonitor
import eu.darken.capod.monitor.core.devicesWithProfiles
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.system.exitProcess
@@ -63,10 +66,10 @@ open class App : Application() {
appScope.launch { widgetManager.refreshWidgets() }
deviceMonitor.devicesWithProfiles()
.distinctUntilChanged()
.distinctUntilChangedBy { devices -> devices.map { it.toWidgetKey() } }
.throttleLatest(1000)
.onEach {
log(TAG) { "Main device changed, refreshing widgets." }
log(TAG, VERBOSE) { "Devices changed, refreshing widgets." }
widgetManager.refreshWidgets()
}
.launchIn(appScope)
@@ -32,14 +32,25 @@ class MediaControl @Inject constructor(
clearRecentCapPause()
}
suspend fun sendPause() {
/**
* Dispatches a MEDIA_PAUSE key event if music is currently playing.
*
* Returns `true` when a key event was actually dispatched (and the 15-second
* [wasRecentlyPausedByCap] window was set), `false` when the call was a no-op because
* nothing was playing. Callers that need to distinguish "we actually paused" from "there
* was nothing to pause" — e.g. the sleep reaction, which gates its notification and
* cooldown on a real pause — should branch on the return value rather than checking
* [isPlaying] themselves to avoid a check-then-act race with the audio system.
*/
suspend fun sendPause(): Boolean {
log(TAG, INFO) { "sendPause()" }
if (!audioManager.isMusicActive) {
log(TAG, INFO) { "Music is not playing, not sending pause" }
return
return false
}
sendKey(KeyEvent.KEYCODE_MEDIA_PAUSE)
markRecentCapPause()
return true
}
suspend fun sendPlayPause() {
@@ -255,11 +255,42 @@ object MockPodDataProvider {
aap = null,
)
/** Dual pods matched to a profile that has no paired Bluetooth device selected. */
fun dualPodMissingPairedDevice(): PodDevice = PodDevice(
profileId = "preview-dual-missing-paired",
ble = MockDualBlePodSnapshot(
_model = PodModel.AIRPODS_PRO2,
_label = "My AirPods Pro",
batteryLeftPodPercent = 0.80f,
batteryRightPodPercent = 0.45f,
_batteryCasePercent = 0.60f,
_isLeftPodCharging = true,
leftPodIcon = R.drawable.device_airpods_pro2_left,
rightPodIcon = R.drawable.device_airpods_pro2_right,
_caseIcon = R.drawable.device_airpods_pro2_case,
),
aap = null,
)
/** Single pod matched to a profile that has no paired Bluetooth device selected. */
fun singlePodMissingPairedDevice(): PodDevice = PodDevice(
profileId = "preview-single-missing-paired",
ble = MockSingleBlePodSnapshot(
_model = PodModel.AIRPODS_MAX,
_label = "AirPods Max",
batteryHeadsetPercent = 0.85f,
_isBeingWorn = true,
),
aap = null,
)
/** Cached-only dual pod — device fully offline, showing last known state. */
fun dualPodCachedOnly(): PodDevice = PodDevice(
profileId = "preview-cached",
ble = null,
aap = null,
profileAddress = "AA:BB:CC:DD:EE:FF",
profileModel = PodModel.AIRPODS_PRO2,
cached = CachedDeviceState(
profileId = "preview-cached",
model = PodModel.AIRPODS_PRO2,
@@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.shareIn
@@ -54,7 +53,6 @@ fun <T> Flow<T>.takeUntilAfter(predicate: suspend (T) -> Boolean) = transformWhi
fun <T> Flow<T>.setupCommonEventHandlers(tag: String, identifier: () -> String) = this
.onStart { log(tag, VERBOSE) { "${identifier()}.onStart()" } }
.onEach { log(tag, VERBOSE) { "${identifier()}.onEach(): $it" } }
.onCompletion { log(tag, VERBOSE) { "${identifier()}.onCompletion()" } }
.catch {
if (it.hasCause(CancellationException::class)) {
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -41,14 +42,17 @@ import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsInfoBox
import eu.darken.capod.common.settings.SettingsSection
import eu.darken.capod.main.ui.devicesettings.cards.AapUnavailableCard
import eu.darken.capod.main.ui.devicesettings.cards.BatteryCard
import eu.darken.capod.main.ui.devicesettings.cards.ControlsCard
import eu.darken.capod.main.ui.devicesettings.cards.DeviceDetailItem
import eu.darken.capod.main.ui.devicesettings.cards.DeviceInfoCard
import eu.darken.capod.main.ui.devicesettings.cards.NoiseControlCard
import eu.darken.capod.main.ui.devicesettings.cards.NotConnectedCard
import eu.darken.capod.main.ui.devicesettings.cards.ReactionsCard
import eu.darken.capod.main.ui.overview.cards.components.MissingPairedDeviceBanner
import eu.darken.capod.main.ui.devicesettings.cards.SoundCard
import eu.darken.capod.main.ui.devicesettings.cards.buildDeviceInfoDetailItems
import eu.darken.capod.main.ui.devicesettings.cards.buildModelLabel
import eu.darken.capod.main.ui.devicesettings.cards.rememberDeviceInfoDetailLabels
import eu.darken.capod.main.ui.devicesettings.components.ConnectedDevicesList
import eu.darken.capod.main.ui.devicesettings.components.EqBarsChart
import eu.darken.capod.main.ui.devicesettings.dialogs.SystemRenameUnavailableDialog
@@ -61,6 +65,10 @@ import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import eu.darken.capod.pods.core.apple.ble.devices.HasStateDetection
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@Composable
fun DeviceSettingsScreenHost(
@@ -78,6 +86,19 @@ fun DeviceSettingsScreenHost(
var showListeningModeCycleDialog by rememberSaveable { mutableStateOf(false) }
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
val offRejectedMessage = stringResource(R.string.device_settings_anc_off_rejected_message)
val chargeCapRejectedMessage = stringResource(R.string.device_settings_charge_cap_rejected_message)
val pendingInfoMessage = stringResource(R.string.device_settings_pending_info)
val hasPendingSettings = state?.device?.hasPendingSettings
var lastPendingState by remember { mutableStateOf<Boolean?>(null) }
LaunchedEffect(hasPendingSettings) {
val current = hasPendingSettings ?: return@LaunchedEffect
val previous = lastPendingState
lastPendingState = current
if (previous == false && current) {
snackbarHostState.showSnackbar(pendingInfoMessage)
}
}
LaunchedEffect(Unit) {
vm.events.collect { event ->
@@ -99,6 +120,10 @@ fun DeviceSettingsScreenHost(
DeviceSettingsViewModel.Event.OffModeRejectedByDevice -> {
snackbarHostState.showSnackbar(offRejectedMessage)
}
DeviceSettingsViewModel.Event.DynamicEndOfChargeRejectedByDevice -> {
snackbarHostState.showSnackbar(chargeCapRejectedMessage)
}
}
}
}
@@ -133,8 +158,10 @@ fun DeviceSettingsScreenHost(
onListeningModeCycleChange = { vm.setListeningModeCycle(it) },
onAllowOffOptionChange = { vm.setAllowOffOption(it) },
onSleepDetectionChange = { vm.setSleepDetection(it) },
onDynamicEndOfChargeChange = { vm.setDynamicEndOfCharge(it) },
onDeviceNameChange = { vm.setDeviceName(it) },
onPressControlsClick = { vm.navToPressControls() },
onEditProfile = { vm.navToEditProfile() },
onForceConnect = { vm.forceConnect() },
onUpgrade = { vm.launchUpgrade() },
onOnePodModeChange = { vm.setOnePodMode(it) },
@@ -146,6 +173,7 @@ fun DeviceSettingsScreenHost(
onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) },
onFixMonitorMode = { vm.setMonitorModeAutomatic() },
onOpenIssueTracker = { vm.openIssueTracker() },
onOpenAapTracker = { vm.openAapCompatibilityTracker() },
)
}
@@ -168,8 +196,10 @@ fun DeviceSettingsScreen(
onListeningModeCycleChange: (Int) -> Unit = {},
onAllowOffOptionChange: (Boolean) -> Unit = {},
onSleepDetectionChange: (Boolean) -> Unit = {},
onDynamicEndOfChargeChange: (Boolean) -> Unit = {},
onDeviceNameChange: (String) -> Unit = {},
onPressControlsClick: () -> Unit = {},
onEditProfile: () -> Unit = {},
onForceConnect: () -> Unit = {},
onUpgrade: () -> Unit = {},
onOnePodModeChange: (Boolean) -> Unit = {},
@@ -181,6 +211,7 @@ fun DeviceSettingsScreen(
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
onFixMonitorMode: () -> Unit = {},
onOpenIssueTracker: () -> Unit = {},
onOpenAapTracker: () -> Unit = {},
) {
val device = state.device
val features = device?.model?.features
@@ -237,86 +268,18 @@ fun DeviceSettingsScreen(
device.firstSeenFormatted(state.now)
} else null
val info = device.deviceInfo
val detailItems = buildList<DeviceDetailItem> {
if (info != null) {
if (info.manufacturer.isNotBlank()) {
add(
DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_manufacturer_label),
info.manufacturer
)
)
}
if (info.serialNumber.isNotBlank()) {
add(
DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_serial_label),
info.serialNumber
)
)
}
val hasFirmware = info.firmwareVersion.isNotBlank()
val hasBuild = !info.buildNumber.isNullOrBlank()
if (hasFirmware && hasBuild) {
add(
DeviceDetailItem.Paired(
start = DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_firmware_label),
info.firmwareVersion
),
end = DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_build_label),
info.buildNumber!!
),
)
)
} else if (hasFirmware) {
add(
DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_firmware_label),
info.firmwareVersion
)
)
} else if (hasBuild) {
add(
DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_build_label),
info.buildNumber!!
)
)
}
val hasLeft = !info.leftEarbudSerial.isNullOrBlank()
val hasRight = !info.rightEarbudSerial.isNullOrBlank()
if (hasLeft && hasRight) {
add(
DeviceDetailItem.Paired(
start = DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_left_serial_label),
info.leftEarbudSerial!!
),
end = DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_right_serial_label),
info.rightEarbudSerial!!
),
)
)
} else if (hasLeft) {
add(
DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_left_serial_label),
info.leftEarbudSerial!!
)
)
} else if (hasRight) {
add(
DeviceDetailItem.Single(
stringResource(R.string.device_settings_info_right_serial_label),
info.rightEarbudSerial!!
)
)
}
}
val locale = LocalConfiguration.current.locales[0]
val zoneId = ZoneId.systemDefault()
val dateFormatter = remember(locale, zoneId) {
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(locale)
.withZone(zoneId)
}
val detailItems = buildDeviceInfoDetailItems(
info = info,
labels = rememberDeviceInfoDetailLabels(),
formatDate = { instant -> dateFormatter.format(instant) },
)
DeviceInfoCard(
deviceInfo = device.deviceInfo,
modelLabel = buildModelLabel(device),
@@ -331,27 +294,46 @@ fun DeviceSettingsScreen(
}
}
// Not connected info — BLE live but no AAP connection
if (device != null && device.ble != null && !device.isAapConnected && device.address != null) {
if (state.isClassicallyConnected) {
// Device is connected for audio but AAP isn't available — show passive info
item("aap_unavailable_info") {
AapUnavailableCard()
}
} else {
// Device is nearby but not connected — prompt user to connect
item("not_connected_info") {
NotConnectedCard(
isNudgeAvailable = state.isNudgeAvailable,
isForceConnecting = state.isForceConnecting,
onConnect = onForceConnect,
)
}
// Profile has no paired Bluetooth device — supersedes all other state cards
if (device != null && !device.hasSelectedPairedDevice) {
item("missing_paired_device") {
MissingPairedDeviceBanner(
onClick = onEditProfile,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}
// ── Reactions (per-profile, not gated on AAP) ─────────────────
if (device != null && features != null) {
// Not nearby — no live BLE; settings require the device to be present
if (device != null && device.hasSelectedPairedDevice &&
device.ble == null && !state.isClassicallyConnected
) {
item("not_nearby_info") {
SettingsInfoBox(
title = stringResource(R.string.device_settings_not_nearby_label),
text = stringResource(R.string.device_settings_not_nearby_description),
)
}
}
// Not connected info — device is nearby but not connected; prompt user to connect
if (device != null && device.hasSelectedPairedDevice &&
device.ble != null && !device.isAapConnected && device.address != null &&
!state.isClassicallyConnected
) {
item("not_connected_info") {
NotConnectedCard(
isNudgeAvailable = state.isNudgeAvailable,
isForceConnecting = state.isForceConnecting,
onConnect = onForceConnect,
)
}
}
// ── Reactions (gated on classic connection — needs phone to be the audio target) ──
if (device != null && device.hasSelectedPairedDevice &&
features != null && state.isClassicallyConnected
) {
item("reactions_section") {
ReactionsCard(
device = device,
@@ -374,15 +356,7 @@ fun DeviceSettingsScreen(
}
// Settings — only show when AAP is connected
if (features != null && device.isAapConnected) {
if (device.hasPendingSettings == true) {
item("pending_info") {
SettingsInfoBox(
text = stringResource(R.string.device_settings_pending_info),
)
}
}
if (features != null && device.isAapConnected && device.hasSelectedPairedDevice) {
// ── Noise Control ────────────────────────────
if (features.hasAncControl && device.ancMode != null) {
@@ -449,6 +423,18 @@ fun DeviceSettingsScreen(
}
}
// ── Battery ──────────────────────────────────
if (features.hasDynamicEndOfCharge && device.dynamicEndOfCharge != null) {
item("battery_section") {
BatteryCard(
device = device,
features = features,
enabled = enabled,
onDynamicEndOfChargeChange = onDynamicEndOfChargeChange,
)
}
}
// ── Connections ───────────────────────────────
val connectedDevices = device.connectedDevices
if (connectedDevices != null && connectedDevices.devices.isNotEmpty()) {
@@ -462,13 +448,22 @@ fun DeviceSettingsScreen(
}
}
// EQ visualization (debug only)
val eqBands = device.eqBands
if (eu.darken.capod.BuildConfig.DEBUG && eqBands != null && eqBands.sets.isNotEmpty()) {
// PME Config visualization (debug only, opcode 0x53).
// PME = Personal Medical Equipment (hearing-aid band gains for iOS
// 18.1+ AirPods Pro 2 hearing-aid mode). The bar chart reuses the
// EQ visualization because the data shape (per-ear × per-profile ×
// band gains) renders identically; hidden on all-zero payloads, which
// just mean the user hasn't configured a hearing-aid profile yet.
val pmeConfig = device.pmeConfig
if (eu.darken.capod.BuildConfig.DEBUG &&
pmeConfig != null &&
pmeConfig.sets.isNotEmpty() &&
!pmeConfig.isAllZero
) {
item("eq_section") {
SettingsSection(title = stringResource(R.string.device_settings_eq_label)) {
EqBarsChart(
sets = eqBands.sets,
sets = pmeConfig.sets,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
@@ -476,6 +471,16 @@ fun DeviceSettingsScreen(
}
}
// Advanced settings unavailable — phone's Bluetooth lacks AAP support; passive info, shown last
if (device != null && device.hasSelectedPairedDevice &&
device.ble != null && !device.isAapConnected && device.address != null &&
state.isClassicallyConnected
) {
item("aap_unavailable_info") {
AapUnavailableCard(onOpenTracker = onOpenAapTracker)
}
}
item("bottom_spacer") {
Spacer(modifier = Modifier.height(16.dp))
}
@@ -496,6 +501,12 @@ internal fun previewFullState(isPro: Boolean) = DeviceSettingsViewModel.State(
manufacturer = "Apple Inc.",
serialNumber = "W5J7KV0N04",
firmwareVersion = "7A305",
hardwareVersion = "1.0.0",
leftEarbudSerial = "H3KL7HR926JY",
rightEarbudSerial = "H3KL2AYL26K0",
marketingVersion = "8454624",
leftEarbudFirstPaired = Instant.ofEpochSecond(1697480211L),
rightEarbudFirstPaired = Instant.ofEpochSecond(1697480211L),
),
settings = mapOf(
AapSetting.AncMode::class to AapSetting.AncMode(
@@ -520,11 +531,13 @@ internal fun previewFullState(isPro: Boolean) = DeviceSettingsViewModel.State(
muteMic = AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS,
endCall = AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS,
),
AapSetting.DynamicEndOfCharge::class to AapSetting.DynamicEndOfCharge(enabled = true),
),
),
),
now = MOCK_NOW,
isPro = isPro,
isClassicallyConnected = true,
)
@Preview2
@@ -590,3 +603,15 @@ private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper {
onNavigateUp = {},
)
}
@Preview2
@Composable
private fun DeviceSettingsMissingPairedDevicePreview() = PreviewWrapper {
DeviceSettingsScreen(
state = DeviceSettingsViewModel.State(
device = MockPodDataProvider.dualPodMissingPairedDevice(),
now = MOCK_NOW,
),
onNavigateUp = {},
)
}
@@ -79,6 +79,7 @@ class DeviceSettingsViewModel @Inject constructor(
data class SendFailed(val command: AapCommand, val message: String?) : Event
data object SystemRenameUnavailable : Event
data object OffModeRejectedByDevice : Event
data object DynamicEndOfChargeRejectedByDevice : Event
}
val events = SingleEventFlow<Event>()
@@ -91,6 +92,16 @@ class DeviceSettingsViewModel @Inject constructor(
}
}
}
launch {
aapManager.settingRejectedEvents.collect { (address, command) ->
if (address != currentAddress()) return@collect
when (command) {
is AapCommand.SetDynamicEndOfCharge ->
events.tryEmit(Event.DynamicEndOfChargeRejectedByDevice)
else -> Unit // Other rejected commands handled elsewhere (e.g. ANC OFF)
}
}
}
}
val state = targetProfileId.flatMapLatest { profileId ->
@@ -283,7 +294,16 @@ class DeviceSettingsViewModel @Inject constructor(
}
}
fun setSleepDetection(enabled: Boolean) = send(AapCommand.SetSleepDetection(enabled))
fun setSleepDetection(enabled: Boolean) = launch {
log(TAG, INFO) { "setSleepDetection($enabled)" }
if (enabled && !upgradeRepo.isPro()) {
navTo(Nav.Main.Upgrade)
return@launch
}
sendInternal(AapCommand.SetSleepDetection(enabled))
}
fun setDynamicEndOfCharge(enabled: Boolean) = send(AapCommand.SetDynamicEndOfCharge(enabled))
fun setDeviceName(name: String) = launch {
val address = currentAddress() ?: return@launch
@@ -422,6 +442,12 @@ class DeviceSettingsViewModel @Inject constructor(
navTo(Nav.Main.PressControls(profileId = profileId))
}
fun navToEditProfile() = launch {
log(TAG, INFO) { "navToEditProfile()" }
val profileId = targetProfileId.value ?: return@launch
navTo(Nav.Main.DeviceProfileCreation(profileId = profileId))
}
fun launchUpgrade() {
log(TAG, INFO) { "launchUpgrade()" }
navTo(Nav.Main.Upgrade)
@@ -431,6 +457,10 @@ class DeviceSettingsViewModel @Inject constructor(
webpageTool.open("https://github.com/d4rken-org/capod/issues")
}
fun openAapCompatibilityTracker() {
webpageTool.open("https://github.com/d4rken-org/capod/issues/538")
}
companion object {
private val TAG = logTag("DeviceSettings", "VM")
private const val OFF_BIT = 0x01
@@ -9,7 +9,9 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -18,7 +20,9 @@ import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
@Composable
internal fun AapUnavailableCard() {
internal fun AapUnavailableCard(
onOpenTracker: () -> Unit = {},
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
@@ -36,6 +40,13 @@ internal fun AapUnavailableCard() {
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
TextButton(
onClick = onOpenTracker,
modifier = Modifier.align(Alignment.End),
) {
Text(stringResource(R.string.device_settings_aap_unavailable_action))
}
}
}
}
@@ -0,0 +1,66 @@
package eu.darken.capod.main.ui.devicesettings.cards
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.BatteryChargingFull
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.settings.SettingsSection
import eu.darken.capod.common.settings.SettingsSwitchItem
import eu.darken.capod.main.ui.devicesettings.previewFullState
import eu.darken.capod.monitor.core.PodDevice
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
/**
* Apple's "Optimized Charge Limit" toggle (AAP setting 0x3B), shown for models that advertise
* [PodModel.Features.hasDynamicEndOfCharge]. The wire format follows the Apple-bool convention
* used by every other boolean setting.
*/
@Composable
internal fun BatteryCard(
device: PodDevice,
features: PodModel.Features,
enabled: Boolean,
onDynamicEndOfChargeChange: (Boolean) -> Unit = {},
) {
if (!features.hasDynamicEndOfCharge) return
val cap = device.dynamicEndOfCharge ?: return
SettingsSection(title = stringResource(R.string.device_settings_category_battery_label)) {
SettingsSwitchItem(
icon = Icons.TwoTone.BatteryChargingFull,
title = stringResource(R.string.device_settings_charge_cap_label),
subtitle = stringResource(R.string.device_settings_charge_cap_description),
checked = cap.enabled,
onCheckedChange = onDynamicEndOfChargeChange,
enabled = enabled,
)
}
}
@Preview2
@Composable
private fun BatteryCardEnabledPreview() = PreviewWrapper {
val state = previewFullState(isPro = true)
val device = state.device!!
BatteryCard(
device = device,
features = PodModel.Features(hasDynamicEndOfCharge = true),
enabled = true,
)
}
@Preview2
@Composable
private fun BatteryCardDisabledPreview() = PreviewWrapper {
val state = previewFullState(isPro = true)
val device = state.device!!
BatteryCard(
device = device,
features = PodModel.Features(hasDynamicEndOfCharge = true),
enabled = false,
)
}
@@ -18,6 +18,11 @@ import androidx.compose.ui.unit.dp
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
@Composable
internal fun DeviceInfoBottomSheet(
@@ -28,57 +33,110 @@ internal fun DeviceInfoBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
Text(
text = stringResource(R.string.device_settings_info_details_label),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 12.dp),
)
items.forEach { item ->
when (item) {
is DeviceDetailItem.Single -> InfoRow(label = item.label, value = item.value)
is DeviceDetailItem.Paired -> Row(modifier = Modifier.fillMaxWidth()) {
InfoRow(
label = item.start.label,
value = item.start.value,
modifier = Modifier.weight(1f),
)
InfoRow(
label = item.end.label,
value = item.end.value,
modifier = Modifier.weight(1f),
textAlign = TextAlign.End,
)
}
DeviceInfoDetailsContent(items)
}
}
@Composable
private fun DeviceInfoDetailsContent(items: List<DeviceDetailItem>) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
Text(
text = stringResource(R.string.device_settings_info_details_label),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 12.dp),
)
items.forEach { item ->
when (item) {
is DeviceDetailItem.Single -> InfoRow(label = item.label, value = item.value)
is DeviceDetailItem.Paired -> Row(modifier = Modifier.fillMaxWidth()) {
InfoRow(
label = item.start.label,
value = item.start.value,
modifier = Modifier.weight(1f),
)
InfoRow(
label = item.end.label,
value = item.end.value,
modifier = Modifier.weight(1f),
textAlign = TextAlign.End,
)
}
}
}
}
}
private val previewDateFormatter: DateTimeFormatter =
DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.US).withZone(ZoneOffset.UTC)
@Preview2
@Composable
private fun DeviceInfoBottomSheetPreview() = PreviewWrapper {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Device Details",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 12.dp),
)
InfoRow(label = "Manufacturer", value = "Apple Inc.")
InfoRow(label = "Serial Number", value = "W5J7KV0N04")
Row(modifier = Modifier.fillMaxWidth()) {
InfoRow(label = "Firmware", value = "7A305", modifier = Modifier.weight(1f))
InfoRow(label = "Build", value = "8454624", modifier = Modifier.weight(1f), textAlign = TextAlign.End)
}
Row(modifier = Modifier.fillMaxWidth()) {
InfoRow(label = "Left Pod Serial", value = "H3KL7HR926JY", modifier = Modifier.weight(1f))
InfoRow(label = "Right Pod Serial", value = "H3KL2AYL26K0", modifier = Modifier.weight(1f), textAlign = TextAlign.End)
}
}
private fun DeviceInfoBottomSheetPreviewSameDayPairing() = PreviewWrapper {
val pairedAt = Instant.parse("2023-10-16T12:00:00Z")
val info = AapDeviceInfo(
name = "AirPods Pro",
modelNumber = "A2699",
manufacturer = "Apple Inc.",
serialNumber = "W5J7KV0N04",
firmwareVersion = "81.2675000075000000.6814",
hardwareVersion = "1.0.0",
leftEarbudSerial = "H3KL7HR926JY",
rightEarbudSerial = "H3KL2AYL26K0",
marketingVersion = "8454768",
leftEarbudFirstPaired = pairedAt,
rightEarbudFirstPaired = pairedAt,
)
DeviceInfoDetailsContent(
buildDeviceInfoDetailItems(info, rememberDeviceInfoDetailLabels()) {
previewDateFormatter.format(it)
},
)
}
@Preview2
@Composable
private fun DeviceInfoBottomSheetPreviewMismatchedPairing() = PreviewWrapper {
val info = AapDeviceInfo(
name = "AirPods Pro",
modelNumber = "A2699",
manufacturer = "Apple Inc.",
serialNumber = "W5J7KV0N04",
firmwareVersion = "81.2675000075000000.6814",
firmwareVersionPending = "82.1000000075000000.7000",
hardwareVersion = "1.0.0",
leftEarbudSerial = "H3KL7HR926JY",
rightEarbudSerial = "H3KL2AYL26K0",
marketingVersion = "8454768",
leftEarbudFirstPaired = Instant.parse("2024-02-14T12:00:00Z"),
rightEarbudFirstPaired = Instant.parse("2023-10-16T12:00:00Z"),
)
DeviceInfoDetailsContent(
buildDeviceInfoDetailItems(info, rememberDeviceInfoDetailLabels()) {
previewDateFormatter.format(it)
},
)
}
@Preview2
@Composable
private fun DeviceInfoBottomSheetPreviewOneSidedPairing() = PreviewWrapper {
val info = AapDeviceInfo(
name = "AirPods Pro",
modelNumber = "A2699",
manufacturer = "Apple Inc.",
serialNumber = "W5J7KV0N04",
firmwareVersion = "7A305",
leftEarbudSerial = "H3KL7HR926JY",
leftEarbudFirstPaired = Instant.parse("2023-10-16T12:00:00Z"),
)
DeviceInfoDetailsContent(
buildDeviceInfoDetailItems(info, rememberDeviceInfoDetailLabels()) {
previewDateFormatter.format(it)
},
)
}
@@ -252,7 +252,7 @@ private fun DeviceInfoCardFullPreview() = PreviewWrapper {
firmwareVersion = "7A305",
leftEarbudSerial = "H3KL7HR926JY",
rightEarbudSerial = "H3KL2AYL26K0",
buildNumber = "8454624",
marketingVersion = "8454624",
),
modelLabel = "AirPods Pro 2 (A2699)",
systemBluetoothName = "AirPods Pro",
@@ -0,0 +1,86 @@
package eu.darken.capod.main.ui.devicesettings.cards
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.darken.capod.R
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
import java.time.Instant
@Composable
internal fun rememberDeviceInfoDetailLabels() = DeviceInfoDetailLabels(
manufacturer = stringResource(R.string.device_settings_info_manufacturer_label),
hardware = stringResource(R.string.device_settings_info_hardware_label),
serial = stringResource(R.string.device_settings_info_serial_label),
firmware = stringResource(R.string.device_settings_info_firmware_label),
firmwarePending = stringResource(R.string.device_settings_info_firmware_pending_label),
build = stringResource(R.string.device_settings_info_build_label),
leftSerial = stringResource(R.string.device_settings_info_left_serial_label),
rightSerial = stringResource(R.string.device_settings_info_right_serial_label),
leftBonded = stringResource(R.string.device_settings_info_left_bonded_label),
rightBonded = stringResource(R.string.device_settings_info_right_bonded_label),
)
internal data class DeviceInfoDetailLabels(
val manufacturer: String,
val hardware: String,
val serial: String,
val firmware: String,
val firmwarePending: String,
val build: String,
val leftSerial: String,
val rightSerial: String,
val leftBonded: String,
val rightBonded: String,
)
internal fun buildDeviceInfoDetailItems(
info: AapDeviceInfo?,
labels: DeviceInfoDetailLabels,
formatDate: (Instant) -> String,
): List<DeviceDetailItem> {
if (info == null) return emptyList()
return buildList {
info.manufacturer.takeIf { it.isNotBlank() }?.let {
add(DeviceDetailItem.Single(labels.manufacturer, it))
}
info.hardwareVersion?.takeIf { it.isNotBlank() }?.let {
add(DeviceDetailItem.Single(labels.hardware, it))
}
info.serialNumber.takeIf { it.isNotBlank() }?.let {
add(DeviceDetailItem.Single(labels.serial, it))
}
info.firmwareVersion.takeIf { it.isNotBlank() }?.let {
add(DeviceDetailItem.Single(labels.firmware, it))
}
info.firmwareVersionPending?.takeIf { it.isNotBlank() }?.let {
add(DeviceDetailItem.Single(labels.firmwarePending, it))
}
info.marketingVersion?.takeIf { it.isNotBlank() }?.let {
add(DeviceDetailItem.Single(labels.build, it))
}
val leftSerial = info.leftEarbudSerial?.takeIf { it.isNotBlank() }
val rightSerial = info.rightEarbudSerial?.takeIf { it.isNotBlank() }
when {
leftSerial != null && rightSerial != null -> add(
DeviceDetailItem.Paired(
start = DeviceDetailItem.Single(labels.leftSerial, leftSerial),
end = DeviceDetailItem.Single(labels.rightSerial, rightSerial),
)
)
leftSerial != null -> add(DeviceDetailItem.Single(labels.leftSerial, leftSerial))
rightSerial != null -> add(DeviceDetailItem.Single(labels.rightSerial, rightSerial))
}
val leftBonded = info.leftEarbudFirstPaired?.let(formatDate)
val rightBonded = info.rightEarbudFirstPaired?.let(formatDate)
when {
leftBonded != null && rightBonded != null -> add(
DeviceDetailItem.Paired(
start = DeviceDetailItem.Single(labels.leftBonded, leftBonded),
end = DeviceDetailItem.Single(labels.rightBonded, rightBonded),
)
)
leftBonded != null -> add(DeviceDetailItem.Single(labels.leftBonded, leftBonded))
rightBonded != null -> add(DeviceDetailItem.Single(labels.rightBonded, rightBonded))
}
}
}
@@ -140,6 +140,7 @@ internal fun ReactionsCard(
checked = sleepDet.enabled,
onCheckedChange = onSleepDetectionChange,
enabled = enabled,
requiresUpgrade = !isPro,
)
if (sleepDet.enabled) {
SettingsInfoBox(
@@ -24,7 +24,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.BatteryChargingFull
import androidx.compose.material.icons.twotone.GridView
import androidx.compose.material.icons.twotone.Tune
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
@@ -52,6 +51,7 @@ import eu.darken.capod.main.ui.overview.cards.components.BatteryCapsule
import eu.darken.capod.main.ui.overview.cards.components.CompactBatterySummary
import eu.darken.capod.main.ui.overview.cards.components.DebugSection
import eu.darken.capod.main.ui.overview.cards.components.DeviceConnectionBadge
import eu.darken.capod.main.ui.overview.cards.components.MissingPairedDeviceBanner
import eu.darken.capod.main.ui.overview.cards.components.SignalIndicator
import eu.darken.capod.main.ui.overview.cards.components.StatusChip
import eu.darken.capod.main.ui.overview.cards.components.StatusChipRow
@@ -61,6 +61,7 @@ import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.monitor.core.PodDevice
import eu.darken.capod.monitor.core.cachedBatteryFormatted
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods.LidState
@@ -175,15 +176,15 @@ fun DualPodsCard(
)
}
}
if (device.profileId != null && device.address == null && onEditProfile != null) {
IconButton(onClick = onEditProfile) {
Icon(
imageVector = Icons.TwoTone.Warning,
contentDescription = stringResource(R.string.overview_card_missing_paired_device_cd),
tint = MaterialTheme.colorScheme.error,
)
}
}
}
if (!isCollapsed
&& device.profileId != null
&& !device.hasSelectedPairedDevice
&& onEditProfile != null
) {
Spacer(modifier = Modifier.height(12.dp))
MissingPairedDeviceBanner(onClick = onEditProfile)
}
if (isCollapsed) {
@@ -227,7 +228,8 @@ private fun ColumnScope.DualPodsCardExpanded(
PodGauge(
iconRes = device.leftPodIcon,
batteryPercent = device.batteryLeft.toBatteryFloat(),
isCharging = device.isLeftPodCharging ?: false,
chargingState = device.leftPodChargingState
?: device.isLeftPodCharging?.let { if (it) AapPodState.ChargingState.CHARGING else null },
isInEar = device.isLeftInEar ?: false,
showEarDetection = device.hasEarDetection && device.hasDualPods,
isMicrophone = device.isLeftPodMicrophone ?: false,
@@ -238,7 +240,8 @@ private fun ColumnScope.DualPodsCardExpanded(
PodGauge(
iconRes = device.rightPodIcon,
batteryPercent = device.batteryRight.toBatteryFloat(),
isCharging = device.isRightPodCharging ?: false,
chargingState = device.rightPodChargingState
?: device.isRightPodCharging?.let { if (it) AapPodState.ChargingState.CHARGING else null },
isInEar = device.isRightInEar ?: false,
showEarDetection = device.hasEarDetection && device.hasDualPods,
isMicrophone = device.isRightPodMicrophone ?: false,
@@ -292,7 +295,7 @@ private fun ColumnScope.DualPodsCardExpanded(
private fun PodGauge(
iconRes: Int,
batteryPercent: Float,
isCharging: Boolean,
chargingState: AapPodState.ChargingState?,
isInEar: Boolean,
showEarDetection: Boolean,
isMicrophone: Boolean,
@@ -370,12 +373,13 @@ private fun PodGauge(
// Status chips
StatusChipRow(
isCharging = isCharging,
chargingState = chargingState,
isInEar = isInEar,
showEarDetection = showEarDetection,
isMicrophone = isMicrophone,
showMicrophone = showMicrophone,
chargingLabel = stringResource(R.string.pods_charging_label),
chargingOptimizedLabel = stringResource(R.string.pods_charging_optimized_label),
inEarLabel = stringResource(R.string.pods_inear_label),
microphoneLabel = stringResource(R.string.pods_microphone_label),
)
@@ -420,11 +424,17 @@ private fun CaseRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (device.isCaseCharging == true) {
StatusChip(
when (val caseState = device.caseChargingState
?: device.isCaseCharging?.let { if (it) AapPodState.ChargingState.CHARGING else null }) {
AapPodState.ChargingState.CHARGING_OPTIMIZED -> StatusChip(
icon = Icons.TwoTone.BatteryChargingFull,
label = stringResource(R.string.pods_charging_optimized_label),
)
AapPodState.ChargingState.CHARGING -> StatusChip(
icon = Icons.TwoTone.BatteryChargingFull,
label = stringResource(R.string.pods_charging_label),
)
else -> Unit
}
val lidState = device.caseLidState
@@ -490,7 +500,7 @@ private fun DualPodsCardCollapsedPreview() = PreviewWrapper {
@Composable
private fun DualPodsCardMissingAddressPreview() = PreviewWrapper {
DualPodsCard(
device = MockPodDataProvider.dualPodMonitoredMixed(),
device = MockPodDataProvider.dualPodMissingPairedDevice(),
showDebug = false,
now = SystemTimeSource.now(),
onEditProfile = {},
@@ -24,7 +24,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.BatteryChargingFull
import androidx.compose.material.icons.twotone.Hearing
import androidx.compose.material.icons.twotone.Tune
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
@@ -50,6 +49,7 @@ import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector
import eu.darken.capod.main.ui.overview.cards.components.CompactBatterySummary
import eu.darken.capod.main.ui.overview.cards.components.DebugSection
import eu.darken.capod.main.ui.overview.cards.components.DeviceConnectionBadge
import eu.darken.capod.main.ui.overview.cards.components.MissingPairedDeviceBanner
import eu.darken.capod.main.ui.overview.cards.components.SignalIndicator
import eu.darken.capod.main.ui.overview.cards.components.StatusChip
import eu.darken.capod.common.SystemTimeSource
@@ -58,6 +58,7 @@ import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.monitor.core.PodDevice
import eu.darken.capod.monitor.core.cachedBatteryFormatted
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
import java.time.Instant
@@ -157,15 +158,15 @@ fun SinglePodsCard(
)
}
}
if (device.profileId != null && device.address == null && onEditProfile != null) {
IconButton(onClick = onEditProfile) {
Icon(
imageVector = Icons.TwoTone.Warning,
contentDescription = stringResource(R.string.overview_card_missing_paired_device_cd),
tint = MaterialTheme.colorScheme.error,
)
}
}
}
if (!isCollapsed
&& device.profileId != null
&& !device.hasSelectedPairedDevice
&& onEditProfile != null
) {
Spacer(modifier = Modifier.height(12.dp))
MissingPairedDeviceBanner(onClick = onEditProfile)
}
if (isCollapsed) {
@@ -267,11 +268,17 @@ private fun ColumnScope.SinglePodsCardExpanded(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (device.isHeadsetBeingCharged == true) {
StatusChip(
when (val headsetState = device.headsetChargingState
?: device.isHeadsetBeingCharged?.let { if (it) AapPodState.ChargingState.CHARGING else null }) {
AapPodState.ChargingState.CHARGING_OPTIMIZED -> StatusChip(
icon = Icons.TwoTone.BatteryChargingFull,
label = stringResource(R.string.pods_charging_optimized_label),
)
AapPodState.ChargingState.CHARGING -> StatusChip(
icon = Icons.TwoTone.BatteryChargingFull,
label = stringResource(R.string.pods_charging_label),
)
else -> Unit
}
if (device.isBeingWorn == true) {
StatusChip(
@@ -359,7 +366,7 @@ private fun SinglePodsCardCollapsedPreview() = PreviewWrapper {
@Composable
private fun SinglePodsCardMissingAddressPreview() = PreviewWrapper {
SinglePodsCard(
device = MockPodDataProvider.singlePodMonitored(),
device = MockPodDataProvider.singlePodMissingPairedDevice(),
showDebug = false,
now = SystemTimeSource.now(),
onEditProfile = {},
@@ -0,0 +1,63 @@
package eu.darken.capod.main.ui.overview.cards.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
@Composable
fun MissingPairedDeviceBanner(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
onClick = onClick,
color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp),
modifier = modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) {},
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.padding(end = 10.dp)
.size(20.dp),
)
Text(
text = stringResource(R.string.overview_card_missing_paired_device),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f),
modifier = Modifier.weight(1f),
)
}
}
}
@Preview2
@Composable
private fun MissingPairedDeviceBannerPreview() = PreviewWrapper {
MissingPairedDeviceBanner(onClick = {})
}
@@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.pods.core.apple.aap.AapPodState
@Composable
fun StatusChip(
@@ -60,12 +61,13 @@ fun StatusChip(
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun StatusChipRow(
isCharging: Boolean,
chargingState: AapPodState.ChargingState?,
isInEar: Boolean,
showEarDetection: Boolean,
isMicrophone: Boolean,
showMicrophone: Boolean,
chargingLabel: String,
chargingOptimizedLabel: String,
inEarLabel: String,
microphoneLabel: String,
modifier: Modifier = Modifier,
@@ -75,11 +77,16 @@ fun StatusChipRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (isCharging) {
StatusChip(
when (chargingState) {
AapPodState.ChargingState.CHARGING_OPTIMIZED -> StatusChip(
icon = Icons.TwoTone.BatteryChargingFull,
label = chargingOptimizedLabel,
)
AapPodState.ChargingState.CHARGING -> StatusChip(
icon = Icons.TwoTone.BatteryChargingFull,
label = chargingLabel,
)
else -> Unit
}
if (showMicrophone && isMicrophone) {
StatusChip(
@@ -106,12 +113,29 @@ private fun StatusChipChargingPreview() = PreviewWrapper {
@Composable
private fun StatusChipRowAllPreview() = PreviewWrapper {
StatusChipRow(
isCharging = true,
chargingState = AapPodState.ChargingState.CHARGING,
isInEar = true,
showEarDetection = true,
isMicrophone = true,
showMicrophone = true,
chargingLabel = "Charging",
chargingOptimizedLabel = "Optimized",
inEarLabel = "In Ear",
microphoneLabel = "Mic",
)
}
@Preview2
@Composable
private fun StatusChipRowOptimizedPreview() = PreviewWrapper {
StatusChipRow(
chargingState = AapPodState.ChargingState.CHARGING_OPTIMIZED,
isInEar = false,
showEarDetection = true,
isMicrophone = true,
showMicrophone = true,
chargingLabel = "Charging",
chargingOptimizedLabel = "Optimized",
inEarLabel = "In Ear",
microphoneLabel = "Mic",
)
@@ -100,6 +100,13 @@ fun AcknowledgementsScreen(
onClick = { onOpenUrl("https://github.com/furiousMAC/continuity") },
)
}
item {
SettingsBaseItem(
title = "apple-wireshark",
subtitle = "Thanks to Pablo Aul for the Wireshark dissector catalog of the AAP/AACP protocol.",
onClick = { onOpenUrl("https://github.com/pabloaul/apple-wireshark") },
)
}
item {
SettingsBaseItem(
title = "crowdin.com",
@@ -110,13 +117,6 @@ fun AcknowledgementsScreen(
item {
SettingsCategoryHeader(text = stringResource(R.string.settings_licenses_label))
}
item {
SettingsBaseItem(
title = "Glide",
subtitle = "An image loading and caching library for Android focused on smooth scrolling. (Multiple licenses)",
onClick = { onOpenUrl("https://github.com/bumptech/glide") },
)
}
item {
SettingsBaseItem(
title = "Material Design Icons",
@@ -145,6 +145,13 @@ fun AcknowledgementsScreen(
onClick = { onOpenUrl("https://github.com/kavishdevar/librepods") },
)
}
item {
SettingsBaseItem(
title = "apple-wireshark",
subtitle = "Wireshark dissector plugins for Apple protocols. (GPL-3.0)",
onClick = { onOpenUrl("https://github.com/pabloaul/apple-wireshark") },
)
}
item {
SettingsBaseItem(
title = "Kotlin",
@@ -166,13 +173,6 @@ fun AcknowledgementsScreen(
onClick = { onOpenUrl("https://source.android.com/source/licenses.html") },
)
}
item {
SettingsBaseItem(
title = "Android",
subtitle = "The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.",
onClick = { onOpenUrl("https://developer.android.com/distribute/tools/promote/brand.html") },
)
}
}
}
}
@@ -52,7 +52,11 @@ class AncGlanceWidget : GlanceAppWidget() {
appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
log(TAG, VERBOSE) { "provideGlance(appWidgetId=$appWidgetId)" }
initialIsPro = ep.upgradeRepo().isPro()
initialProfileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
ep.widgetSettings().migrateLegacyConfigIfNeeded(
appWidgetId,
AppWidgetManager.getInstance(context).getAppWidgetOptions(appWidgetId),
)
initialProfileId = ep.widgetSettings().getWidgetConfig(appWidgetId).profileId
cachedDevice = initialProfileId?.let { ep.deviceMonitor().getDeviceForProfile(it) }
} catch (e: Exception) {
log(TAG, ERROR) { "provideGlance setup failed: ${e.asLog()}" }
@@ -80,10 +84,9 @@ class AncGlanceWidget : GlanceAppWidget() {
val heightDp = LocalSize.current.height
val state = try {
val profileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
val theme = WidgetTheme.fromBundle(
AppWidgetManager.getInstance(context).getAppWidgetOptions(appWidgetId)
)
val config = ep.widgetSettings().getWidgetConfig(appWidgetId)
val profileId = config.profileId
val theme = config.theme
val isPro = upgradeInfo?.isPro ?: initialIsPro
@@ -158,7 +161,7 @@ class AncGlanceWidget : GlanceAppWidget() {
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
while (70 * n - 30 <= size) {
++n
}
return n - 1
@@ -53,7 +53,7 @@ class AncModeActionCallback : ActionCallback {
val ep = EntryPointAccessors.fromApplication(context, AncModeCallbackEntryPoint::class.java)
val profileId = ep.widgetSettings().getWidgetProfile(widgetId)
val profileId = ep.widgetSettings().getWidgetConfig(widgetId).profileId
if (profileId == null) {
log(TAG, ERROR) { "onAction: no profile for widgetId=$widgetId" }
return
@@ -52,17 +52,23 @@ class BatteryGlanceWidget : GlanceAppWidget() {
appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
log(TAG, VERBOSE) { "provideGlance(appWidgetId=$appWidgetId)" }
initialIsPro = ep.upgradeRepo().isPro()
initialProfileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
ep.widgetSettings().migrateLegacyConfigIfNeeded(
appWidgetId,
AppWidgetManager.getInstance(context).getAppWidgetOptions(appWidgetId),
)
initialProfileId = ep.widgetSettings().getWidgetConfig(appWidgetId).profileId
cachedDevice = initialProfileId?.let { ep.deviceMonitor().getDeviceForProfile(it) }
} catch (e: Exception) {
log(TAG, ERROR) { "provideGlance setup failed: ${e.asLog()}" }
provideContent {
val layout = BatteryLayout.forCells(getCellsForSize(LocalSize.current.width.value.toInt()))
GlanceWidgetContent(
state = WidgetRenderState.Message(
theme = WidgetTheme.DEFAULT,
resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
layout = layout,
primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label),
),
context = context,
@@ -77,12 +83,12 @@ class BatteryGlanceWidget : GlanceAppWidget() {
val profiles by ep.deviceProfilesRepo().profiles.collectAsState(initial = emptyList())
val upgradeInfo by ep.upgradeRepo().upgradeInfo.collectAsState(initial = null)
val widthDp = LocalSize.current.width
val layout = BatteryLayout.forCells(getCellsForSize(widthDp.value.toInt()))
val state = try {
val profileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
val theme = WidgetTheme.fromBundle(
AppWidgetManager.getInstance(context).getAppWidgetOptions(appWidgetId)
)
val config = ep.widgetSettings().getWidgetConfig(appWidgetId)
val profileId = config.profileId
val theme = config.theme
val isPro = upgradeInfo?.isPro ?: initialIsPro
@@ -93,8 +99,6 @@ class BatteryGlanceWidget : GlanceAppWidget() {
profiles.firstOrNull { it.id == pid }?.label
}
val isWide = getCellsForSize(widthDp.value.toInt()) >= 5
WidgetRenderStateMapper.map(
context = context,
device = device,
@@ -102,7 +106,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
isPro = isPro,
hasConfiguredProfile = profileId != null,
profileLabel = profileLabel,
isWide = isWide,
layout = layout,
)
} catch (e: Exception) {
log(TAG, ERROR) { "provideGlance failed: ${e.asLog()}" }
@@ -111,6 +115,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
layout = layout,
primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label),
)
}
@@ -137,7 +142,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
*/
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
while (70 * n - 30 <= size) {
++n
}
return n - 1
@@ -25,6 +25,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.darken.capod.R
@@ -87,86 +88,51 @@ private fun DualPodPreview(
val iconColor = Color(state.resolvedIconColor)
val iconTint = ColorFilter.tint(iconColor)
if (state.isWide) {
// Wide layout: left | case | right in a horizontal row
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
// Left pod
PodItemRow(
icon = state.leftIcon,
percent = state.leftPercent,
charging = state.leftCharging,
inEar = state.leftInEar,
when (state.layout) {
BatteryLayout.WIDE -> {
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
PodItemRow(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textColor, iconTint, iconSize = 40, modifier = Modifier.padding(end = 12.dp))
PodItemRow(state.caseIcon, state.casePercent, state.caseCharging, false, textColor, iconTint, iconSize = 40, modifier = Modifier.padding(end = 12.dp))
PodItemRow(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textColor, iconTint, iconSize = 40)
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
iconTint = iconTint,
iconSize = 40,
modifier = Modifier.padding(end = 12.dp),
)
// Case
PodItemRow(
icon = state.caseIcon,
percent = state.casePercent,
charging = state.caseCharging,
inEar = false,
textColor = textColor,
iconTint = iconTint,
iconSize = 40,
modifier = Modifier.padding(end = 12.dp),
)
// Right pod
PodItemRow(
icon = state.rightIcon,
percent = state.rightPercent,
charging = state.rightCharging,
inEar = state.rightInEar,
textColor = textColor,
iconTint = iconTint,
iconSize = 40,
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp),
)
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp),
)
}
} else {
// Compact layout: vertical stack
WidgetContainer(bgColor = bgColor, modifier = modifier) {
PodItemRow(
icon = state.leftIcon,
percent = state.leftPercent,
charging = state.leftCharging,
inEar = state.leftInEar,
textColor = textColor,
iconTint = iconTint,
)
PodItemRow(
icon = state.rightIcon,
percent = state.rightPercent,
charging = state.rightCharging,
inEar = state.rightInEar,
textColor = textColor,
iconTint = iconTint,
)
PodItemRow(
icon = state.caseIcon,
percent = state.casePercent,
charging = state.caseCharging,
inEar = false,
textColor = textColor,
iconTint = iconTint,
)
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
BatteryLayout.NARROW -> {
WidgetContainer(bgColor = bgColor, modifier = modifier) {
PodItemRow(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textColor, iconTint)
PodItemRow(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textColor, iconTint)
PodItemRow(state.caseIcon, state.casePercent, state.caseCharging, false, textColor, iconTint)
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
}
}
BatteryLayout.TINY_COLUMN -> {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
TinyPodItem(state.leftIcon, state.leftPercent, textColor, iconTint)
TinyPodItem(state.rightIcon, state.rightPercent, textColor, iconTint)
TinyPodItem(state.caseIcon, state.casePercent, textColor, iconTint)
}
}
}
}
@@ -180,45 +146,60 @@ private fun SinglePodPreview(
val textColor = Color(state.resolvedTextColor)
val iconTint = ColorFilter.tint(Color(state.resolvedIconColor))
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(state.batteryIcon),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatPercent(state.percent.toBatteryOrNull()),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
painter = painterResource(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
}
if (state.worn) {
Image(
painter = painterResource(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
when (state.layout) {
BatteryLayout.WIDE, BatteryLayout.NARROW -> {
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(state.batteryIcon),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatPercent(state.percent.toBatteryOrNull()),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
painter = painterResource(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
}
if (state.worn) {
Image(
painter = painterResource(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
}
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
}
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
BatteryLayout.TINY_COLUMN -> {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
TinyPodItem(state.batteryIcon, state.percent, textColor, iconTint)
}
}
}
}
@@ -229,17 +210,25 @@ private fun MessagePreview(
) {
val bgColor = Color(state.resolvedBgColor)
val textColor = Color(state.resolvedTextColor)
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
WidgetContainer(bgColor = bgColor, modifier = modifier) {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = if (isCompact) 4.dp else 16.dp,
verticalPadding = if (isCompact) 0.dp else 4.dp,
) {
Text(
text = state.primaryText,
fontSize = 12.sp,
fontSize = if (isCompact) 10.sp else 12.sp,
fontWeight = FontWeight.Bold,
color = textColor,
textAlign = TextAlign.Center,
maxLines = if (isCompact) 2 else Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
)
if (state.secondaryText != null) {
if (!isCompact && state.secondaryText != null) {
Text(
text = state.secondaryText,
fontSize = 12.sp,
@@ -260,11 +249,17 @@ private fun LoadingPreview(
) {
val bgColor = Color(state.resolvedBgColor)
val textColor = Color(state.resolvedTextColor)
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
WidgetContainer(bgColor = bgColor, modifier = modifier) {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = if (isCompact) 4.dp else 16.dp,
verticalPadding = if (isCompact) 0.dp else 4.dp,
) {
Text(
text = "",
fontSize = 12.sp,
fontSize = if (isCompact) 10.sp else 12.sp,
fontWeight = FontWeight.Bold,
color = textColor,
textAlign = TextAlign.Center,
@@ -277,13 +272,15 @@ private fun LoadingPreview(
private fun WidgetContainer(
bgColor: Color,
modifier: Modifier = Modifier,
horizontalPadding: Dp = 16.dp,
verticalPadding: Dp = 4.dp,
content: @Composable () -> Unit,
) {
Column(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(bgColor)
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
@@ -338,6 +335,34 @@ private fun PodItemRow(
}
}
@Composable
private fun TinyPodItem(
icon: Int,
percent: Float,
textColor: Color,
iconTint: ColorFilter,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(icon),
contentDescription = null,
modifier = Modifier.size(14.dp),
colorFilter = iconTint,
)
Text(
text = formatPercent(percent.toBatteryOrNull()),
fontSize = 12.sp,
color = textColor,
maxLines = 1,
modifier = Modifier.padding(start = 2.dp),
)
}
}
@Composable
private fun DeviceLabel(
label: String?,
@@ -363,12 +388,30 @@ private fun formatPercent(percent: Float?): String {
@Preview2
@Composable
private fun PreviewDualCompact() = PreviewWrapper {
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod())
private fun PreviewDualTiny11() = PreviewWrapper {
ComposeWidgetPreview(
state = WidgetRenderState.previewDualPod(layout = BatteryLayout.TINY_COLUMN),
modifier = Modifier.size(40.dp),
)
}
@Preview2
@Composable
private fun PreviewDualTinyTall() = PreviewWrapper {
ComposeWidgetPreview(
state = WidgetRenderState.previewDualPod(layout = BatteryLayout.TINY_COLUMN),
modifier = Modifier.size(width = 40.dp, height = 110.dp),
)
}
@Preview2
@Composable
private fun PreviewDualNarrow() = PreviewWrapper {
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod(layout = BatteryLayout.NARROW))
}
@Preview2
@Composable
private fun PreviewDualWide() = PreviewWrapper {
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod(isWide = true))
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod(layout = BatteryLayout.WIDE))
}
@@ -3,6 +3,7 @@ package eu.darken.capod.main.ui.widget
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.ColorFilter
@@ -53,31 +54,47 @@ private fun GlanceDualPod(
state: WidgetRenderState.DualPod,
clickModifier: GlanceModifier,
) {
val textStyle = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
)
val iconTint = ColorFilter.tint(fixedColor(state.resolvedIconColor))
if (state.isWide) {
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint, iconSize = 40)
when (state.layout) {
BatteryLayout.WIDE -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint, iconSize = 40)
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
} else {
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint)
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint)
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint)
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
BatteryLayout.NARROW -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint)
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint)
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint)
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
}
BatteryLayout.TINY_COLUMN -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
GlanceTinyPodItem(state.leftIcon, state.leftPercent, textStyle, iconTint)
GlanceTinyPodItem(state.rightIcon, state.rightPercent, textStyle, iconTint)
GlanceTinyPodItem(state.caseIcon, state.casePercent, textStyle, iconTint)
}
}
}
}
@@ -87,47 +104,60 @@ private fun GlanceSinglePod(
state: WidgetRenderState.SinglePod,
clickModifier: GlanceModifier,
) {
val textStyle = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
)
val iconTint = ColorFilter.tint(fixedColor(state.resolvedIconColor))
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
provider = ImageProvider(state.batteryIcon),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatGlancePercent(state.percent.toBatteryOrNull()),
style = textStyle,
modifier = GlanceModifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
when (state.layout) {
BatteryLayout.WIDE, BatteryLayout.NARROW -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
provider = ImageProvider(state.batteryIcon),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatGlancePercent(state.percent.toBatteryOrNull()),
style = textStyle,
modifier = GlanceModifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
}
if (state.worn) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
}
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
if (state.worn) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
}
BatteryLayout.TINY_COLUMN -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
GlanceTinyPodItem(state.batteryIcon, state.percent, textStyle, iconTint)
}
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
}
@@ -136,20 +166,29 @@ private fun GlanceMessage(
state: WidgetRenderState.Message,
clickModifier: GlanceModifier,
) {
val textStyle = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
)
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
val fontSize = if (isCompact) 10.sp else 12.sp
val hPad = if (isCompact) 4.dp else 16.dp
val vPad = if (isCompact) 0.dp else 4.dp
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = hPad,
verticalPadding = vPad,
) {
Text(
text = state.primaryText,
style = textStyle,
style = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = fontSize,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
modifier = GlanceModifier.fillMaxWidth(),
maxLines = if (isCompact) 2 else Int.MAX_VALUE,
)
if (state.secondaryText != null) {
if (!isCompact && state.secondaryText != null) {
Text(
text = state.secondaryText,
style = TextStyle(
@@ -169,12 +208,18 @@ private fun GlanceLoading(
state: WidgetRenderState.Loading,
clickModifier: GlanceModifier,
) {
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = if (isCompact) 4.dp else 16.dp,
verticalPadding = if (isCompact) 0.dp else 4.dp,
) {
Text(
text = "",
style = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
fontSize = if (isCompact) 10.sp else 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
@@ -187,13 +232,15 @@ private fun GlanceLoading(
private fun GlanceWidgetRoot(
bgColor: Int,
clickModifier: GlanceModifier,
horizontalPadding: Dp = 16.dp,
verticalPadding: Dp = 4.dp,
content: @Composable () -> Unit,
) {
Column(
modifier = clickModifier
.fillMaxSize()
.background(fixedColor(bgColor))
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -246,6 +293,33 @@ private fun GlancePodItem(
}
}
@Composable
private fun GlanceTinyPodItem(
icon: Int,
percent: Float,
textStyle: TextStyle,
iconTint: ColorFilter,
modifier: GlanceModifier = GlanceModifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
provider = ImageProvider(icon),
contentDescription = null,
modifier = GlanceModifier.size(14.dp),
colorFilter = iconTint,
)
Text(
text = formatGlancePercent(percent.toBatteryOrNull()),
style = textStyle,
maxLines = 1,
modifier = GlanceModifier.padding(start = 4.dp),
)
}
}
@Composable
private fun GlanceDeviceLabel(
label: String?,
@@ -0,0 +1,10 @@
package eu.darken.capod.main.ui.widget
import eu.darken.capod.profiles.core.ProfileId
import kotlinx.serialization.Serializable
@Serializable
data class WidgetConfig(
val profileId: ProfileId? = null,
val theme: WidgetTheme = WidgetTheme(),
)
@@ -47,19 +47,21 @@ class WidgetConfigurationViewModel @Inject constructor(
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
log(TAG) { "Invalid widget ID" }
} else {
widgetSettings.migrateLegacyConfigIfNeeded(widgetId, appWidgetManager.getAppWidgetOptions(widgetId))
}
}
private val selectedProfile = MutableStateFlow(widgetSettings.getWidgetProfile(widgetId))
private val initialTheme: WidgetTheme = run {
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@run WidgetTheme.DEFAULT
WidgetTheme.fromBundle(appWidgetManager.getAppWidgetOptions(widgetId))
private val initialConfig: WidgetConfig = run {
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@run WidgetConfig()
widgetSettings.getWidgetConfig(widgetId)
}
private val forceCustomMode = MutableStateFlow(WidgetTheme.matchPreset(initialTheme) == null)
private val selectedProfile = MutableStateFlow(initialConfig.profileId)
private val currentTheme = MutableStateFlow(initialTheme)
private val forceCustomMode = MutableStateFlow(WidgetTheme.matchPreset(initialConfig.theme) == null)
private val currentTheme = MutableStateFlow(initialConfig.theme)
private val visibleProfiles = deviceProfilesRepo.profiles.map { profiles ->
if (isAncWidget) profiles.filter { it.model.features.hasAncControl } else profiles
@@ -152,18 +154,16 @@ class WidgetConfigurationViewModel @Inject constructor(
}
fun confirmSelection() {
val selectedProfile = selectedProfile.value
if (selectedProfile != null) {
log(TAG, INFO) { "confirmSelection(widgetId=$widgetId, selectedProfile=$selectedProfile)" }
widgetSettings.saveWidgetProfile(widgetId, selectedProfile)
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
log(TAG, INFO) { "confirmSelection: invalid widget ID, skipping save" }
return
}
// Save theme to AppWidgetOptions bundle
val theme = currentTheme.value
log(TAG, INFO) { "confirmSelection: saving theme=$theme" }
val options = appWidgetManager.getAppWidgetOptions(widgetId)
theme.toBundle(options)
appWidgetManager.updateAppWidgetOptions(widgetId, options)
val config = WidgetConfig(
profileId = selectedProfile.value,
theme = currentTheme.value,
)
log(TAG, INFO) { "confirmSelection(widgetId=$widgetId, config=$config)" }
widgetSettings.saveWidgetConfig(widgetId, config)
}
companion object {
@@ -0,0 +1,40 @@
package eu.darken.capod.main.ui.widget
import eu.darken.capod.monitor.core.PodDevice
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
/**
* Snapshot of the [PodDevice] fields that affect widget rendering. Used to
* gate widget refreshes so RSSI / reliability / scan-counter churn doesn't
* cause a refresh on every BLE advertisement.
*/
internal data class WidgetDeviceKey(
val profileId: String?,
val model: PodModel,
val batteryLeft: Float?,
val batteryRight: Float?,
val batteryCase: Float?,
val batteryHeadset: Float?,
val isLeftPodCharging: Boolean?,
val isRightPodCharging: Boolean?,
val isCaseCharging: Boolean?,
val isHeadsetBeingCharged: Boolean?,
val ancMode: AapSetting.AncMode.Value?,
val pendingAncMode: AapSetting.AncMode.Value?,
)
internal fun PodDevice.toWidgetKey(): WidgetDeviceKey = WidgetDeviceKey(
profileId = profileId,
model = model,
batteryLeft = batteryLeft,
batteryRight = batteryRight,
batteryCase = batteryCase,
batteryHeadset = batteryHeadset,
isLeftPodCharging = isLeftPodCharging,
isRightPodCharging = isRightPodCharging,
isCaseCharging = isCaseCharging,
isHeadsetBeingCharged = isHeadsetBeingCharged,
ancMode = ancMode?.current,
pendingAncMode = pendingAncMode,
)
@@ -3,9 +3,6 @@ package eu.darken.capod.main.ui.widget
import android.content.Context
import androidx.glance.appwidget.updateAll
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import javax.inject.Inject
import javax.inject.Singleton
@@ -16,12 +13,7 @@ class WidgetManager @Inject constructor(
) {
suspend fun refreshWidgets() {
log(TAG, VERBOSE) { "refreshWidgets()" }
BatteryGlanceWidget().updateAll(context)
AncGlanceWidget().updateAll(context)
}
companion object {
val TAG = logTag("Widget", "Manager")
}
}
@@ -4,18 +4,33 @@ import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import eu.darken.capod.R
enum class BatteryLayout {
TINY_COLUMN,
NARROW,
WIDE;
companion object {
fun forCells(widthCells: Int): BatteryLayout = when {
widthCells <= 1 -> TINY_COLUMN
widthCells >= 5 -> WIDE
else -> NARROW
}
}
}
sealed class WidgetRenderState {
abstract val theme: WidgetTheme
@get:ColorInt abstract val resolvedBgColor: Int
@get:ColorInt abstract val resolvedTextColor: Int
@get:ColorInt abstract val resolvedIconColor: Int
abstract val layout: BatteryLayout
data class DualPod(
override val theme: WidgetTheme,
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
val isWide: Boolean,
override val layout: BatteryLayout,
val deviceLabel: String?,
@DrawableRes val leftIcon: Int,
val leftPercent: Float,
@@ -35,6 +50,7 @@ sealed class WidgetRenderState {
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
override val layout: BatteryLayout,
val deviceLabel: String?,
@DrawableRes val headsetIcon: Int,
val percent: Float,
@@ -48,6 +64,7 @@ sealed class WidgetRenderState {
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
override val layout: BatteryLayout,
val primaryText: String,
val secondaryText: String? = null,
) : WidgetRenderState()
@@ -57,6 +74,7 @@ sealed class WidgetRenderState {
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
override val layout: BatteryLayout,
) : WidgetRenderState()
companion object {
@@ -65,13 +83,13 @@ sealed class WidgetRenderState {
@ColorInt bgColor: Int = 0xFFFFFFFF.toInt(),
@ColorInt textColor: Int = 0xFF1E1E1E.toInt(),
@ColorInt iconColor: Int = 0xFF1E1E1E.toInt(),
isWide: Boolean = false,
layout: BatteryLayout = BatteryLayout.NARROW,
): DualPod = DualPod(
theme = theme,
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
isWide = isWide,
layout = layout,
deviceLabel = "My AirPods Pro",
leftIcon = R.drawable.device_airpods_pro2_left,
leftPercent = 0.85f,
@@ -85,5 +103,25 @@ sealed class WidgetRenderState {
casePercent = 1.0f,
caseCharging = false,
)
fun previewSinglePod(
theme: WidgetTheme = WidgetTheme.DEFAULT,
@ColorInt bgColor: Int = 0xFFFFFFFF.toInt(),
@ColorInt textColor: Int = 0xFF1E1E1E.toInt(),
@ColorInt iconColor: Int = 0xFF1E1E1E.toInt(),
layout: BatteryLayout = BatteryLayout.NARROW,
): SinglePod = SinglePod(
theme = theme,
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
deviceLabel = "My AirPods Max",
headsetIcon = R.drawable.device_airpods_max,
percent = 0.72f,
batteryIcon = R.drawable.ic_baseline_battery_3_bar_24,
charging = false,
worn = true,
)
}
}
@@ -18,7 +18,7 @@ object WidgetRenderStateMapper {
isPro: Boolean,
hasConfiguredProfile: Boolean,
profileLabel: String?,
isWide: Boolean = false,
layout: BatteryLayout = BatteryLayout.NARROW,
): WidgetRenderState {
val bgColor = resolvedBgColor(context, theme)
val textColor = resolvedTextColor(context, theme)
@@ -30,6 +30,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
primaryText = context.getString(R.string.upgrade_capod_label),
secondaryText = context.getString(R.string.upgrade_capod_description),
)
@@ -39,7 +40,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
isWide = isWide,
layout = layout,
deviceLabel = profileLabel ?: device.getLabel(context),
leftIcon = device.leftPodIcon,
leftPercent = device.batteryLeft.toBatteryFloat(),
@@ -59,6 +60,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
deviceLabel = profileLabel ?: device.getLabel(context),
headsetIcon = device.iconRes,
percent = device.batteryHeadset.toBatteryFloat(),
@@ -72,6 +74,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
primaryText = context.getString(R.string.pods_unknown_label),
)
@@ -86,6 +89,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
primaryText = context.getString(messageRes),
)
}
@@ -1,6 +1,7 @@
package eu.darken.capod.main.ui.widget
import android.content.Context
import android.os.Bundle
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
@@ -11,15 +12,19 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.profiles.core.ProfileId
import eu.darken.capod.common.serialization.SerializationCapod
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WidgetSettings @Inject constructor(
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
@SerializationCapod private val json: Json,
) {
private val Context.dataStore by preferencesDataStore(
@@ -29,32 +34,76 @@ class WidgetSettings @Inject constructor(
private val dataStore: DataStore<Preferences> get() = context.dataStore
fun saveWidgetProfile(widgetId: Int, profileId: ProfileId) {
log(TAG, VERBOSE) { "saveWidgetProfile(widgetId=$widgetId, profileId=$profileId)" }
runBlocking {
dataStore.edit { it[stringPreferencesKey(getWidgetProfileKey(widgetId))] = profileId }
fun getWidgetConfig(widgetId: Int): WidgetConfig {
val raw = runBlocking { dataStore.data.first()[configKey(widgetId)] }
if (raw == null) {
log(TAG, VERBOSE) { "getWidgetConfig(widgetId=$widgetId) = absent → default" }
return WidgetConfig()
}
return try {
json.decodeFromString<WidgetConfig>(raw).also {
log(TAG, VERBOSE) { "getWidgetConfig(widgetId=$widgetId) = $it" }
}
} catch (e: SerializationException) {
log(TAG) { "getWidgetConfig(widgetId=$widgetId): malformed JSON, returning default ($e)" }
WidgetConfig()
}
}
fun getWidgetProfile(widgetId: Int): ProfileId? {
val profileId = runBlocking {
dataStore.data.first()[stringPreferencesKey(getWidgetProfileKey(widgetId))]
fun saveWidgetConfig(widgetId: Int, config: WidgetConfig) {
log(TAG, VERBOSE) { "saveWidgetConfig(widgetId=$widgetId, config=$config)" }
val encoded = json.encodeToString(config)
runBlocking {
dataStore.edit { prefs ->
prefs[configKey(widgetId)] = encoded
prefs.remove(legacyProfileKey(widgetId))
}
}
}
/**
* One-shot migration from the legacy storage layout (separate `widget_profile_<id>` DataStore key
* + per-widget theme in `AppWidgetManager.getAppWidgetOptions()`) to the unified
* `widget_config_<id>` JSON. Marker-guarded inside the same `dataStore.edit {}` block so
* concurrent saves can't be overwritten.
*
* Pre-reboot the legacy options Bundle still carries the user's theme; post-reboot it does not
* (Android only persists the standard `OPTION_APPWIDGET_*` keys).
*/
fun migrateLegacyConfigIfNeeded(widgetId: Int, legacyOptions: Bundle) {
val legacyTheme = WidgetTheme.fromLegacyBundleOrNull(legacyOptions)
runBlocking {
dataStore.edit { prefs ->
if (prefs[configKey(widgetId)] != null) return@edit
val legacyProfileId = prefs[legacyProfileKey(widgetId)]
if (legacyProfileId == null && legacyTheme == null) return@edit
val migrated = WidgetConfig(
profileId = legacyProfileId,
theme = legacyTheme ?: WidgetTheme(),
)
log(TAG) { "migrateLegacyConfigIfNeeded(widgetId=$widgetId): writing $migrated" }
prefs[configKey(widgetId)] = json.encodeToString(migrated)
prefs.remove(legacyProfileKey(widgetId))
}
}
log(TAG, VERBOSE) { "getWidgetProfile(widgetId=$widgetId) = $profileId" }
return profileId
}
fun removeWidget(widgetId: Int) {
log(TAG, VERBOSE) { "removeWidget(widgetId=$widgetId)" }
runBlocking {
dataStore.edit { it.remove(stringPreferencesKey(getWidgetProfileKey(widgetId))) }
dataStore.edit { prefs ->
prefs.remove(configKey(widgetId))
prefs.remove(legacyProfileKey(widgetId))
}
}
}
private fun getWidgetProfileKey(widgetId: Int): String = "$WIDGET_PROFILE_PREFIX$widgetId"
private fun configKey(widgetId: Int) = stringPreferencesKey("$WIDGET_CONFIG_PREFIX$widgetId")
private fun legacyProfileKey(widgetId: Int) = stringPreferencesKey("$LEGACY_WIDGET_PROFILE_PREFIX$widgetId")
companion object {
private const val WIDGET_PROFILE_PREFIX = "widget_profile_"
private const val WIDGET_CONFIG_PREFIX = "widget_config_"
private const val LEGACY_WIDGET_PROFILE_PREFIX = "widget_profile_"
private val TAG = logTag("Widget", "Settings")
}
}
@@ -4,7 +4,9 @@ import android.graphics.Color
import android.os.Bundle
import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils
import kotlinx.serialization.Serializable
@Serializable
data class WidgetTheme(
@ColorInt val backgroundColor: Int? = null,
@ColorInt val foregroundColor: Int? = null,
@@ -36,7 +38,18 @@ data class WidgetTheme(
val DEFAULT = WidgetTheme()
fun fromBundle(bundle: Bundle): WidgetTheme {
/**
* Reads a [WidgetTheme] from the legacy `AppWidgetManager.getAppWidgetOptions()` Bundle.
* Returns `null` if the Bundle holds none of the legacy keys — distinguishing "system reset
* the Bundle to system-only keys" from "user picked Material You + 255".
*/
fun fromLegacyBundleOrNull(bundle: Bundle): WidgetTheme? {
val hasLegacyKey = bundle.containsKey(KEY_THEME_MODE)
|| bundle.containsKey(KEY_BG_ALPHA)
|| bundle.containsKey(KEY_SHOW_LABEL)
|| bundle.containsKey(KEY_CUSTOM_BG)
|| bundle.containsKey(KEY_CUSTOM_FG)
if (!hasLegacyKey) return null
val mode = bundle.getString(KEY_THEME_MODE, MODE_MATERIAL_YOU)
val showLabel = bundle.getBoolean(KEY_SHOW_LABEL, true)
val alpha = bundle.getInt(KEY_BG_ALPHA, 255).coerceIn(0, 255)
@@ -71,25 +84,4 @@ data class WidgetTheme(
}
}
fun toBundle(bundle: Bundle) {
if (backgroundColor == null && foregroundColor == null) {
bundle.putString(KEY_THEME_MODE, MODE_MATERIAL_YOU)
bundle.remove(KEY_CUSTOM_BG)
bundle.remove(KEY_CUSTOM_FG)
} else {
bundle.putString(KEY_THEME_MODE, MODE_CUSTOM)
if (backgroundColor != null) {
bundle.putInt(KEY_CUSTOM_BG, backgroundColor)
} else {
bundle.remove(KEY_CUSTOM_BG)
}
if (foregroundColor != null) {
bundle.putInt(KEY_CUSTOM_FG, foregroundColor)
} else {
bundle.remove(KEY_CUSTOM_FG)
}
}
bundle.putInt(KEY_BG_ALPHA, backgroundAlpha.coerceIn(0, 255))
bundle.putBoolean(KEY_SHOW_LABEL, showDeviceLabel)
}
}
@@ -65,6 +65,13 @@ data class PodDevice(
val model: PodModel get() = ble?.model ?: profileModel ?: cached?.model ?: PodModel.UNKNOWN
/** Bonded BR/EDR address (from profile). Used for AAP commands. */
val address: BluetoothAddress? get() = ble?.meta?.profile?.address ?: profileAddress ?: cached?.address
/**
* True when the matched profile has a selected paired Bluetooth device. Unlike [address],
* this never falls back to the cache, so a profile whose paired device was removed still
* reports `false` even while a stale cached address lingers.
*/
val hasSelectedPairedDevice: Boolean
get() = (ble?.meta?.profile?.address ?: profileAddress) != null
/** BLE scan address (RPA, rotates). */
val bleAddress: BluetoothAddress? get() = ble?.address
val identifier: BlePodSnapshot.Id? get() = ble?.identifier
@@ -78,6 +85,7 @@ data class PodDevice(
val hasEarDetection: Boolean get() = model.features.hasEarDetection
val hasAncControl: Boolean get() = model.features.hasAncControl
val hasDualMicrophone: Boolean get() = ble is HasDualMicrophone
val hasDynamicEndOfCharge: Boolean get() = model.features.hasDynamicEndOfCharge
// Signal / timing
val seenLastAt: Instant?
@@ -179,6 +187,15 @@ data class PodDevice(
val isHeadsetBeingCharged: Boolean?
get() = aap?.isHeadsetCharging ?: (ble as? HasChargeDetection)?.isHeadsetBeingCharged ?: cached?.isHeadsetCharging
// Full per-slot charging state — AAP only (BLE + cache don't carry CHARGING_OPTIMIZED).
// Null means "no live AAP reading", which lets callers avoid showing a stale Optimized chip
// after the device went out of range. Existing isLeftCharging/etc. remain the Boolean
// collapse of CHARGING + CHARGING_OPTIMIZED for everyone who just cares "is it charging".
val leftPodChargingState: AapPodState.ChargingState? get() = aap?.leftChargingState
val rightPodChargingState: AapPodState.ChargingState? get() = aap?.rightChargingState
val caseChargingState: AapPodState.ChargingState? get() = aap?.caseChargingState
val headsetChargingState: AapPodState.ChargingState? get() = aap?.headsetChargingState
// Resolved primary pod: AAP cmd 0x08 preferred, BLE bit 5 fallback.
private val resolvedPrimaryPod: DualBlePodSnapshot.Pod?
get() = aap?.aapPrimaryPod?.pod?.let { aapPod ->
@@ -330,13 +347,16 @@ data class PodDevice(
val sleepDetection: AapSetting.SleepDetection?
get() = aap?.setting()
val dynamicEndOfCharge: AapSetting.DynamicEndOfCharge?
get() = aap?.setting()
val connectedDevices: AapSetting.ConnectedDevices?
get() = aap?.setting()
val audioSource: AapSetting.AudioSource?
get() = aap?.setting()
val eqBands: AapSetting.EqBands?
val pmeConfig: AapSetting.PmeConfig?
get() = aap?.setting()
val deviceInfo: AapDeviceInfo?
@@ -25,7 +25,12 @@ data class CachedDeviceState(
@SerialName("firmwareVersion") val firmwareVersion: String? = null,
@SerialName("leftEarbudSerial") val leftEarbudSerial: String? = null,
@SerialName("rightEarbudSerial") val rightEarbudSerial: String? = null,
@SerialName("buildNumber") val buildNumber: String? = null,
/**
* Formerly known as `buildNumber` — the Wireshark AAP dissector calls this
* "Marketing Version". The `@SerialName("buildNumber")` preserves the wire
* key so cache entries from older CAPod versions keep deserializing.
*/
@SerialName("buildNumber") val marketingVersion: String? = null,
@Serializable(with = InstantEpochMillisSerializer::class)
@SerialName("lastSeenAt") val lastSeenAt: Instant,
) {
@@ -40,7 +45,7 @@ data class CachedDeviceState(
firmwareVersion = firmwareVersion ?: "",
leftEarbudSerial = leftEarbudSerial,
rightEarbudSerial = rightEarbudSerial,
buildNumber = buildNumber,
marketingVersion = marketingVersion,
)
}
@@ -50,7 +50,7 @@ fun PodDevice.toCachedState(
firmwareVersion = liveDeviceInfo?.firmwareVersion ?: existing?.firmwareVersion,
leftEarbudSerial = liveDeviceInfo?.leftEarbudSerial ?: existing?.leftEarbudSerial,
rightEarbudSerial = liveDeviceInfo?.rightEarbudSerial ?: existing?.rightEarbudSerial,
buildNumber = liveDeviceInfo?.buildNumber ?: existing?.buildNumber,
marketingVersion = liveDeviceInfo?.marketingVersion ?: existing?.marketingVersion,
lastSeenAt = seenLastAt ?: now,
)
@@ -11,6 +11,7 @@ import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider
@@ -35,11 +36,13 @@ import eu.darken.capod.monitor.core.primaryDevice
import eu.darken.capod.monitor.ui.MonitorNotifications
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.profiles.core.DeviceProfile
import eu.darken.capod.profiles.core.DeviceProfilesRepo
import eu.darken.capod.reaction.core.autoconnect.AutoConnect
import eu.darken.capod.reaction.core.playpause.PlayPause
import eu.darken.capod.reaction.core.popup.PopUpReaction
import eu.darken.capod.reaction.core.sleep.SleepReaction
import eu.darken.capod.reaction.ui.popup.PopUpWindow
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
@@ -48,6 +51,7 @@ import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
@@ -73,6 +77,7 @@ class MonitorService : Service() {
@Inject lateinit var playPause: PlayPause
@Inject lateinit var autoConnect: AutoConnect
@Inject lateinit var popUpReaction: PopUpReaction
@Inject lateinit var sleepReaction: SleepReaction
@Inject lateinit var popUpWindow: PopUpWindow
@Inject lateinit var profilesRepo: DeviceProfilesRepo
@Inject lateinit var aapConnectionManager: AapConnectionManager
@@ -223,31 +228,19 @@ class MonitorService : Service() {
profilesRepo.profiles,
bluetoothManager.connectedDevices,
aapConnectionManager.allStates,
) { monitorMode, profiles, connectedDevices, aapStates ->
listOf(monitorMode, profiles, connectedDevices, aapStates)
) { mode, profiles, devices, aapStates ->
buildMonitorModeState(mode, profiles, devices, aapStates)
}
}
}
.distinctUntilChanged()
.setupCommonEventHandlers(TAG) { "MonitorMode" }
.flatMapLatest { arguments ->
val monitorMode = arguments[0] as MonitorMode
.flatMapLatest { state ->
log(TAG) { "Monitor mode: ${state.mode}" }
log(TAG) { "connectedAddresses: ${state.connectedAddresses}" }
log(TAG) { "knownAddresses: ${state.knownAddresses}" }
@Suppress("UNCHECKED_CAST")
val profiles = arguments[1] as List<DeviceProfile>
@Suppress("UNCHECKED_CAST")
val devices = arguments[2] as Collection<BluetoothDevice2>
@Suppress("UNCHECKED_CAST")
val aapStates = arguments[3] as Map<*, *>
val connectedAddresses = devices.map { it.address }.toSet()
val knownAddresses = profiles.mapNotNull { it.address }.toSet()
log(TAG) { "Monitor mode: $monitorMode" }
log(TAG) { "connectedAddresses: $connectedAddresses" }
log(TAG) { "knownAddresses: $knownAddresses" }
when (monitorMode) {
when (state.mode) {
MonitorMode.MANUAL -> flow<Unit> {
monitorScope.coroutineContext.cancelChildren()
}
@@ -255,11 +248,11 @@ class MonitorService : Service() {
MonitorMode.ALWAYS -> emptyFlow()
MonitorMode.AUTOMATIC -> flow {
when {
profiles.isEmpty() && devices.isNotEmpty() -> {
!state.hasProfiles && state.connectedAddresses.isNotEmpty() -> {
log(TAG, WARN) { "Main device address not set, staying alive while any is connected" }
}
knownAddresses.any { it in connectedAddresses } || aapStates.isNotEmpty() -> {
state.knownAddresses.any { it in state.connectedAddresses } || state.hasAapSession -> {
log(TAG) { "A device is connected, aborting any timeout." }
}
@@ -302,6 +295,11 @@ class MonitorService : Service() {
.catch { log(TAG, WARN) { "autoConnect failed:\n${it.asLog()}" } }
.launchIn(monitorScope)
sleepReaction.monitor()
.setupCommonEventHandlers(TAG) { "sleepReaction" }
.catch { log(TAG, WARN) { "sleepReaction failed:\n${it.asLog()}" } }
.launchIn(monitorScope)
log(TAG, VERBOSE) { "Monitor job is active" }
monitorJob.join()
log(TAG, VERBOSE) { "Monitor job quit" }
@@ -340,6 +338,27 @@ class MonitorService : Service() {
}
}
internal data class MonitorModeState(
val mode: MonitorMode,
val hasProfiles: Boolean,
val knownAddresses: Set<BluetoothAddress>,
val connectedAddresses: Set<BluetoothAddress>,
val hasAapSession: Boolean,
)
internal fun buildMonitorModeState(
mode: MonitorMode,
profiles: List<DeviceProfile>,
devices: Collection<BluetoothDevice2>,
aapStates: Map<BluetoothAddress, AapPodState>,
): MonitorModeState = MonitorModeState(
mode = mode,
hasProfiles = profiles.isNotEmpty(),
knownAddresses = profiles.mapNotNull { it.address }.toSet(),
connectedAddresses = devices.map { it.address }.toSet(),
hasAapSession = aapStates.isNotEmpty(),
)
private data class NotificationDeviceKey(
val profileId: String?,
val label: String?,
@@ -232,6 +232,7 @@ enum class PodModel(
hasAllowOffOption = true,
hasStemConfig = true,
hasSleepDetection = true,
hasDynamicEndOfCharge = true,
),
modelNumbers = setOf("A3063", "A3064", "A3065"), // earphones
leftPodIconRes = R.drawable.device_airpods_pro2_left,
@@ -558,5 +559,12 @@ enum class PodModel(
val hasAllowOffOption: Boolean = false,
val hasStemConfig: Boolean = false,
val hasSleepDetection: Boolean = false,
/**
* Apple's "Optimized Charge Limit" (AAP setting 0x3B). Distinct from the older
* "Optimized Battery Charging" — that earlier feature isn't exposed as a toggleable
* AAP setting. Enable on any model that's been confirmed (via capture) to push 0x3B
* on connect and accept writes to it.
*/
val hasDynamicEndOfCharge: Boolean = false,
)
}
@@ -64,10 +64,30 @@ class AapConnectionManager @Inject constructor(
private val _stemPressEvents = MutableSharedFlow<Pair<BluetoothAddress, StemPressEvent>>(extraBufferCapacity = 32)
val stemPressEvents: SharedFlow<Pair<BluetoothAddress, StemPressEvent>> = _stemPressEvents.asSharedFlow()
/**
* Emits when a connected device fires a Sleep Detection update (AAP opcode 0x57).
* Address-only — the opaque payload stays logged at engine level per the "never suppress
* protocol logging" convention; downstream consumers (SleepReaction) only need the origin
* address to decide whether to act.
*/
private val _sleepEvents = MutableSharedFlow<BluetoothAddress>(extraBufferCapacity = 16)
val sleepEvents: SharedFlow<BluetoothAddress> = _sleepEvents.asSharedFlow()
/** Emits when a SetAncMode(OFF) command was rejected by the device (inferred by the engine). */
private val _offRejectedEvents = MutableSharedFlow<BluetoothAddress>(extraBufferCapacity = 16)
val offRejectedEvents: SharedFlow<BluetoothAddress> = _offRejectedEvents.asSharedFlow()
/**
* Emits when any setting command failed verification on the device side. Unlike
* [offRejectedEvents] this covers every rejected write (including the ANC-OFF case); UI
* consumers filter by the command type they care about — e.g. the charge-cap toggle shows
* a snackbar only for [AapCommand.SetDynamicEndOfCharge].
*/
private val _settingRejectedEvents =
MutableSharedFlow<Pair<BluetoothAddress, AapCommand>>(extraBufferCapacity = 16)
val settingRejectedEvents: SharedFlow<Pair<BluetoothAddress, AapCommand>> =
_settingRejectedEvents.asSharedFlow()
fun deviceState(address: BluetoothAddress) = _allStates.map { it[address] }
suspend fun connect(
@@ -112,6 +132,14 @@ class AapConnectionManager @Inject constructor(
}
}
// Forward sleep events from this connection (child coroutine). Address-only —
// the opaque payload stays logged at the engine layer.
launch {
connection.sleepEvents.collect {
_sleepEvents.tryEmit(address)
}
}
// Forward OFF-rejection events from this connection (child coroutine)
launch {
connection.offRejected.collect {
@@ -119,6 +147,14 @@ class AapConnectionManager @Inject constructor(
}
}
// Forward generic setting-rejection events from this connection (child coroutine).
// Covers every rejected write — consumers filter by the command they care about.
launch {
connection.settingRejected.collect { command ->
_settingRejectedEvents.tryEmit(address to command)
}
}
connection.state.collect { podState ->
if (podState.connectionState == AapPodState.ConnectionState.DISCONNECTED) {
log(TAG) { "Connection to $address disconnected" }
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.aap
import eu.darken.capod.pods.core.apple.aap.protocol.AapCaseInfo
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import java.time.Instant
@@ -16,6 +17,16 @@ data class AapPodState(
val lastMessageAt: Instant? = null,
val pendingAncMode: AapSetting.AncMode.Value? = null,
val pendingSettingsCount: Int = 0,
/**
* 64-bit features bitmask from the Connect Response (packet type 0x0001).
* Opaque — we don't yet know the bit-to-feature mapping. Logged for future
* correlation work. Null until a Connect Response is seen.
*/
val negotiatedFeatures: ULong? = null,
/** Status field from the Connect Response. 0 = success. Null before Connect Response. */
val connectResponseStatus: Int? = null,
/** Parsed Case Info (message type 0x23), if the device responded to a 0x22 probe. */
val caseInfo: AapCaseInfo? = null,
) {
inline fun <reified T : AapSetting> setting(): T? = settings[T::class] as? T
@@ -42,6 +53,13 @@ data class AapPodState(
val batteryCase: Float? get() = batteries[BatteryType.CASE]?.percent
val batteryHeadset: Float? get() = batteries[BatteryType.SINGLE]?.percent
// Raw charging state per slot — the full enum, not collapsed to a Boolean. Lets the UI
// distinguish CHARGING_OPTIMIZED ("Optimized Charge Limit in effect") from plain CHARGING.
val leftChargingState: ChargingState? get() = batteries[BatteryType.LEFT]?.charging
val rightChargingState: ChargingState? get() = batteries[BatteryType.RIGHT]?.charging
val caseChargingState: ChargingState? get() = batteries[BatteryType.CASE]?.charging
val headsetChargingState: ChargingState? get() = batteries[BatteryType.SINGLE]?.charging
// Charging state from AAP battery
val isLeftCharging: Boolean?
get() = batteries[BatteryType.LEFT]?.let { it.charging == ChargingState.CHARGING || it.charging == ChargingState.CHARGING_OPTIMIZED }
@@ -12,7 +12,8 @@ import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceProfile
import eu.darken.capod.pods.core.apple.aap.protocol.AapFramer
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessage
import eu.darken.capod.pods.core.apple.aap.protocol.AapPacket
import eu.darken.capod.pods.core.apple.aap.protocol.AapSleepEvent
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
import kotlinx.coroutines.CoroutineScope
@@ -45,7 +46,9 @@ internal class AapConnection(
val state: StateFlow<AapPodState> get() = engine.state
val keysReceived: SharedFlow<KeyExchangeResult> get() = engine.keysReceived
val stemPressEvents: SharedFlow<StemPressEvent> get() = engine.stemPressEvents
val sleepEvents: SharedFlow<AapSleepEvent> get() = engine.sleepEvents
val offRejected: SharedFlow<Unit> get() = engine.offRejected
val settingRejected: SharedFlow<AapCommand> get() = engine.settingRejected
private var socket: BluetoothSocket? = null
private var readerJob: Job? = null
@@ -143,11 +146,36 @@ internal class AapConnection(
break
}
// L2CAP SEQPACKET: each read() returns exactly one complete message
// L2CAP SEQPACKET: each read() returns exactly one complete frame
val raw = buf.copyOfRange(0, len)
val message = AapMessage.parse(raw)
if (message != null) {
engine.processMessage(message)
val packet = AapPacket.parse(raw) ?: continue
when (packet) {
is AapPacket.Message -> engine.processMessage(packet)
is AapPacket.ConnectResponse -> {
engine.processConnectResponse(packet)
if (packet.status == 0) dispatchCaseInfoProbe()
}
is AapPacket.Disconnect -> {
log(TAG, Logging.Priority.INFO) {
"Disconnect received: service=0x${"%04X".format(packet.service)} status=0x${"%04X".format(packet.status)}"
}
}
is AapPacket.DisconnectResponse -> {
log(TAG) { "DisconnectResponse received: service=0x${"%04X".format(packet.service)}" }
}
is AapPacket.Connect -> {
// Unexpected — we're the source, not the peer. Log and ignore.
log(TAG, Logging.Priority.WARN) {
"Unexpected Connect packet from peer: service=0x${"%04X".format(packet.service)}"
}
}
is AapPacket.Unknown -> {
val hex = packet.raw.joinToString(" ") { "%02X".format(it) }
log(TAG, Logging.Priority.INFO) {
"Unknown AAP packet type=0x${"%04X".format(packet.packetType)} len=${packet.raw.size} raw=$hex"
}
}
}
}
} catch (e: IOException) {
@@ -158,6 +186,31 @@ internal class AapConnection(
}
}
/**
* Fire-and-forget Case Info request (message type 0x22). Sent after a
* successful Connect Response. Not an AapCommand — bypasses the outbound
* queue, ear-gating, and pending-settings counter. If the device doesn't
* reply, nothing happens; the next connect attempt re-probes.
*/
private suspend fun dispatchCaseInfoProbe() {
val bytes = profile.encodeCaseInfoRequest() ?: return
try {
writeMutex.withLock {
withContext(Dispatchers.IO) {
val sock = socket ?: return@withContext
sock.outputStream.write(bytes)
sock.outputStream.flush()
val hex = bytes.joinToString(" ") { "%02X".format(it) }
log(TAG, Logging.Priority.VERBOSE) { "SEND CaseInfoProbe len=${bytes.size} raw=$hex" }
}
}
} catch (e: Exception) {
// Non-critical: the probe is fire-and-forget; a send failure just means
// we don't get Case Info this session. No retry, no state change.
log(TAG, Logging.Priority.WARN) { "CaseInfoProbe send failed: $e" }
}
}
private fun cleanupSocket() {
try {
socket?.close()
@@ -4,47 +4,88 @@ import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
/**
* Diagnostic-only NUL-delimited segmentation of a 0x1D INFORMATION payload, used for
* issue #173 engraving discovery logging.
* Diagnostic-only segmentation of a 0x1D INFORMATION payload, used for issue #173
* engraving discovery logging.
*
* Schema matches the Wireshark AAP dissector's INFORMATION message:
* - Segments 0-10 are NUL-delimited UTF-8 strings.
* - Segments 11 and 12 are fixed 17-byte UUIDs (may contain 0x00 internally —
* splitting on NUL here would corrupt the offsets for every later segment).
* - Segments 13 and 14 are NUL-delimited ASCII-decimal timestamps.
*
* Payloads that truncate early just produce fewer segments — the helper stays
* best-effort and never throws.
*/
internal object AapDeviceInfoDiagnostics {
private const val UUID_LEN = 17
private const val LAST_STRING_SEGMENT = 10 // after this the UUID blob starts
fun describeSegments(payload: ByteArray): List<DeviceInfoSegment> {
var start = 0
while (start < payload.size) {
val b = payload[start].toInt() and 0xFF
if (b in 0x20..0x7E) break
start++
}
if (start >= payload.size) return emptyList()
var offset = skipBinaryHeader(payload)
if (offset >= payload.size) return emptyList()
val segments = mutableListOf<DeviceInfoSegment>()
var segIndex = 0
var i = start
while (i < payload.size) {
while (i < payload.size && payload[i] == 0x00.toByte()) i++
if (i >= payload.size) break
val segStart = i
while (i < payload.size && payload[i] != 0x00.toByte()) i++
val segBytes = payload.copyOfRange(segStart, i)
val utf8: String? = try {
Charsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(segBytes))
.toString()
} catch (_: CharacterCodingException) {
null
}
val hex = segBytes.joinToString("") { "%02X".format(it) }
segments += DeviceInfoSegment(segIndex, segStart, segBytes.size, utf8, hex)
// Segments 0..10 — NUL-delimited UTF-8 strings
while (segIndex <= LAST_STRING_SEGMENT && offset < payload.size) {
while (offset < payload.size && payload[offset] == 0x00.toByte()) offset++
if (offset >= payload.size) break
val segStart = offset
while (offset < payload.size && payload[offset] != 0x00.toByte()) offset++
segments += buildSegment(segIndex, segStart, payload.copyOfRange(segStart, offset))
segIndex++
}
// Skip the NUL terminator of segment 10 before the UUID blob.
while (offset < payload.size && payload[offset] == 0x00.toByte()) offset++
// Segments 11 and 12 — fixed 17-byte UUIDs, read verbatim.
for (targetIdx in 11..12) {
if (offset + UUID_LEN > payload.size) break
val segBytes = payload.copyOfRange(offset, offset + UUID_LEN)
segments += buildSegment(targetIdx, offset, segBytes)
offset += UUID_LEN
segIndex = targetIdx + 1
}
// Segments 13+ — NUL-delimited timestamps / any trailing strings.
while (offset < payload.size) {
while (offset < payload.size && payload[offset] == 0x00.toByte()) offset++
if (offset >= payload.size) break
val segStart = offset
while (offset < payload.size && payload[offset] != 0x00.toByte()) offset++
segments += buildSegment(segIndex, segStart, payload.copyOfRange(segStart, offset))
segIndex++
}
return segments
}
private fun skipBinaryHeader(payload: ByteArray): Int {
var i = 0
while (i < payload.size) {
val b = payload[i].toInt() and 0xFF
if (b in 0x20..0x7E) return i
i++
}
return payload.size
}
private fun buildSegment(index: Int, offset: Int, bytes: ByteArray): DeviceInfoSegment {
val utf8: String? = try {
Charsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString()
} catch (_: CharacterCodingException) {
null
}
val hex = bytes.joinToString("") { "%02X".format(it) }
return DeviceInfoSegment(index, offset, bytes.size, utf8, hex)
}
}
internal data class DeviceInfoSegment(
@@ -1,10 +1,13 @@
package eu.darken.capod.pods.core.apple.aap.engine
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.pods.core.apple.aap.protocol.AapCaseInfo
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceProfile
import eu.darken.capod.pods.core.apple.aap.protocol.AapDynamicEndOfChargeEvent
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessage
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import eu.darken.capod.pods.core.apple.aap.protocol.AapSleepEvent
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
import kotlin.reflect.KClass
@@ -14,6 +17,9 @@ internal sealed interface AapInboundUpdate {
data class Battery(val batteries: Map<AapPodState.BatteryType, AapPodState.Battery>) : AapInboundUpdate
data class PrivateKeys(val result: KeyExchangeResult) : AapInboundUpdate
data class DeviceInfo(val info: AapDeviceInfo) : AapInboundUpdate
data class CaseInfo(val info: AapCaseInfo) : AapInboundUpdate
data class SleepEvent(val event: AapSleepEvent) : AapInboundUpdate
data class DynamicEndOfChargeEvent(val event: AapDynamicEndOfChargeEvent) : AapInboundUpdate
data class Setting(val key: KClass<out AapSetting>, val value: AapSetting) : AapInboundUpdate
}
@@ -25,6 +31,11 @@ internal class AapInboundInterpreter(
profile.decodeBattery(message)?.let { return AapInboundUpdate.Battery(it) }
profile.decodePrivateKeyResponse(message)?.let { return AapInboundUpdate.PrivateKeys(it) }
profile.decodeDeviceInfo(message)?.let { return AapInboundUpdate.DeviceInfo(it) }
profile.decodeCaseInfo(message)?.let { return AapInboundUpdate.CaseInfo(it) }
profile.decodeSleepEvent(message)?.let { return AapInboundUpdate.SleepEvent(it) }
profile.decodeDynamicEndOfChargeEvent(message)?.let {
return AapInboundUpdate.DynamicEndOfChargeEvent(it)
}
profile.decodeSetting(message)?.let { (key, value) ->
return AapInboundUpdate.Setting(key, value)
}
@@ -35,7 +35,11 @@ internal class AapOutboundController(
runtimeState: OutboundRuntimeState,
command: AapCommand,
): OutboundDecision {
if (command !is AapCommand.SetDeviceName) {
// SetDeviceName and SetDynamicEndOfCharge bypass ear-gating:
// - Rename is a user-initiated metadata change, independent of wear state.
// - Charge cap (setting 0x3B) is toggled while pods sit in the closed case; queueing
// it until worn would make the toggle look broken for its main use case.
if (command !is AapCommand.SetDeviceName && command !is AapCommand.SetDynamicEndOfCharge) {
val earDetection = podState.setting<AapSetting.EarDetection>()
if (earDetection != null && !earDetection.isEitherPodInEar) {
val result = coordinator.enqueue(runtimeState.pendingCommands, command, podState)
@@ -10,7 +10,10 @@ import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceProfile
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessage
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessageType
import eu.darken.capod.pods.core.apple.aap.protocol.AapPacket
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import eu.darken.capod.pods.core.apple.aap.protocol.AapSleepEvent
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
import kotlinx.coroutines.CoroutineScope
@@ -47,10 +50,24 @@ internal class AapSessionEngine(
MutableSharedFlow<StemPressEvent>(extraBufferCapacity = 8, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val stemPressEvents: SharedFlow<StemPressEvent> = _stemPressEvents.asSharedFlow()
private val _sleepEvents =
MutableSharedFlow<AapSleepEvent>(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val sleepEvents: SharedFlow<AapSleepEvent> = _sleepEvents.asSharedFlow()
private val _offRejected =
MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val offRejected: SharedFlow<Unit> = _offRejected.asSharedFlow()
/**
* Fires whenever a write command fails verification after the coordinator's single retry —
* i.e. the device neither echoed the expected state nor retried into a matching one.
* Separate from [offRejected] which is specialised for the ANC-OFF UX path. Consumers filter
* by the command type they care about (e.g. the charge-cap toggle shows a snackbar).
*/
private val _settingRejected =
MutableSharedFlow<AapCommand>(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val settingRejected: SharedFlow<AapCommand> = _settingRejected.asSharedFlow()
private val hidTracker = HidTracker { msg -> log(TAG) { msg } }
private val inboundInterpreter = AapInboundInterpreter(profile)
private val ancController = AapAncController()
@@ -88,6 +105,21 @@ internal class AapSessionEngine(
dispatch(AapEngineEvent.MessageReceived(message))
}
/**
* Handle a Connect Response (packet type 0x0001) from the peer. Stores
* the 64-bit features bitmask in [AapPodState.negotiatedFeatures] for
* future correlation work, but does NOT drive state transitions itself —
* the first subsequent Message packet triggers HANDSHAKING → READY via
* the existing logic.
*
* On non-zero status we log an error and skip the features update; the
* engine stays in HANDSHAKING (no probes should fire until the session
* succeeds).
*/
fun processConnectResponse(packet: AapPacket.ConnectResponse) {
dispatch(AapEngineEvent.ConnectResponseReceived(packet))
}
private fun dispatch(event: AapEngineEvent) {
when (event) {
is AapEngineEvent.SessionStarted -> {
@@ -112,13 +144,36 @@ internal class AapSessionEngine(
}
is AapEngineEvent.MessageReceived -> handleMessageReceived(event.message)
is AapEngineEvent.ConnectResponseReceived -> handleConnectResponse(event.packet)
is AapEngineEvent.InboundUpdateDecoded -> handleInboundUpdate(event.update)
is AapEngineEvent.TimerFired -> handleTimerFired(event.key)
}
}
private fun handleConnectResponse(packet: AapPacket.ConnectResponse) {
if (packet.status != 0) {
log(TAG, ERROR) {
"ConnectResponse failed: status=0x${"%04X".format(packet.status)} " +
"major=${packet.major} minor=${packet.minor} " +
"features=0x${"%016X".format(packet.features.toLong())}"
}
_state.value = _state.value.copy(connectResponseStatus = packet.status)
return
}
log(TAG, INFO) {
"ConnectResponse OK: major=${packet.major} minor=${packet.minor} " +
"features=0x${"%016X".format(packet.features.toLong())}"
}
_state.value = _state.value.copy(
negotiatedFeatures = packet.features,
connectResponseStatus = packet.status,
lastMessageAt = timeSource.now(),
)
}
private fun handleMessageReceived(message: AapMessage) {
if (message.commandType != CMD_HID_DESCRIPTOR) {
if (message.commandType != AapMessageType.BUDDY_COMMAND.value) {
val hex = message.raw.joinToString(" ") { "%02X".format(it) }
log(TAG, VERBOSE) {
"MSG cmd=0x${"%04X".format(message.commandType)} len=${message.raw.size} raw=$hex"
@@ -127,7 +182,7 @@ internal class AapSessionEngine(
}
if (!runtimeState.handshakeResponseReceived &&
message.commandType != CMD_SETTING &&
message.commandType != AapMessageType.CONTROL.value &&
_state.value.connectionState == AapPodState.ConnectionState.HANDSHAKING
) {
runtimeState = runtimeState.copy(handshakeResponseReceived = true)
@@ -135,13 +190,13 @@ internal class AapSessionEngine(
log(TAG) { "Connection READY" }
}
if (message.commandType == CMD_HID_DESCRIPTOR) {
if (message.commandType == AapMessageType.BUDDY_COMMAND.value) {
hidTracker.consume(message.payload)
_state.value = _state.value.copy(lastMessageAt = timeSource.now())
return
}
if (message.commandType == CMD_DEVICE_INFO) {
if (message.commandType == AapMessageType.INFORMATION.value) {
logDeviceInfoDiagnostics(message.payload)
}
@@ -186,6 +241,25 @@ internal class AapSessionEngine(
log(TAG) { "Device info: ${update.info.name} (${update.info.modelNumber})" }
}
is AapInboundUpdate.CaseInfo -> {
_state.value = _state.value.copy(caseInfo = update.info, lastMessageAt = timeSource.now())
val hex = update.info.rawPayload.joinToString(" ") { "%02X".format(it) }
log(TAG, INFO) { "Case info: ${update.info.rawPayload.size}B payload=[$hex]" }
}
is AapInboundUpdate.SleepEvent -> {
_state.value = _state.value.copy(lastMessageAt = timeSource.now())
val hex = update.event.rawPayload.joinToString(" ") { "%02X".format(it) }
log(TAG, INFO) { "Sleep event: ${update.event.rawPayload.size}B payload=[$hex]" }
_sleepEvents.tryEmit(update.event)
}
is AapInboundUpdate.DynamicEndOfChargeEvent -> {
_state.value = _state.value.copy(lastMessageAt = timeSource.now())
val hex = update.event.rawPayload.joinToString(" ") { "%02X".format(it) }
log(TAG, INFO) { "Dynamic EoC event: ${update.event.rawPayload.size}B payload=[$hex]" }
}
is AapInboundUpdate.Setting -> handleSettingUpdate(update.key, update.value)
}
}
@@ -361,10 +435,12 @@ internal class AapSessionEngine(
}
private fun handleRejectedCommand(command: AapCommand?) {
if (command == null) return
if (command is AapCommand.SetAncMode && command.mode == AapSetting.AncMode.Value.OFF) {
applyAncDecision(ancController.onOffRejected(_state.value, runtimeState.anc))
_offRejected.tryEmit(Unit)
}
_settingRejected.tryEmit(command)
}
private fun scheduleTimer(key: EngineTimerKey, delayMs: Long) {
@@ -411,20 +487,24 @@ internal class AapSessionEngine(
"DeviceInfoDump #173: payload=${payload.size} bytes, segments=${segments.size}"
}
segments.forEach { seg ->
// Labels per the Wireshark AAP dissector. The segmenter knows the schema —
// segments 11 and 12 are fixed 17-byte UUIDs.
val label = when (seg.index) {
0 -> "name"
1 -> "modelNumber"
2 -> "manufacturer"
3 -> "serialNumber"
4 -> "firmwareVersion"
5 -> "firmwareVersionDup"
6 -> "protocolVersion"
7 -> "updaterAppId"
5 -> "firmwareVersionPending"
6 -> "hardwareVersion"
7 -> "eaProtocolName"
8 -> "leftEarbudSerial"
9 -> "rightEarbudSerial"
10 -> "buildNumber"
11 -> "encryptedBlob"
12 -> "timestamp"
10 -> "marketingVersion"
11 -> "leftEarbudUuid (17 bytes fixed)"
12 -> "rightEarbudUuid (17 bytes fixed)"
13 -> "leftEarbudFirstPaired"
14 -> "rightEarbudFirstPaired"
else -> "unknown"
}
val rendered = seg.utf8?.let { "\"$it\"" } ?: "<non-utf8>"
@@ -438,7 +518,7 @@ internal class AapSessionEngine(
val payloadHex = message.payload.joinToString(" ") { "%02X".format(it) }
val sendInfo = currentSendDebugInfo()
if (message.commandType == CMD_SETTING && message.payload.size >= 2) {
if (message.commandType == AapMessageType.CONTROL.value && message.payload.size >= 2) {
val settingId = message.payload[0].toInt() and 0xFF
val value = message.payload[1].toInt() and 0xFF
val boolHint = value.appleBoolHint()
@@ -461,7 +541,7 @@ internal class AapSessionEngine(
return
}
if (message.commandType == CMD_CONNECTED_DEVICE && message.payload.size >= 6) {
if (message.commandType == AapMessageType.MAC_ADDRESS.value && message.payload.size >= 6) {
val macRaw = message.payload.formatMac(reverse = false)
val macReversed = message.payload.formatMac(reverse = true)
val tailHex = if (message.payload.size > 6) {
@@ -472,7 +552,7 @@ internal class AapSessionEngine(
_state.value = _state.value.copy(lastMessageAt = timeSource.now())
log(TAG, VERBOSE) {
buildString {
append("Known cmd=0x000C")
append("Known cmd=0x000C (${AapMessageType.MAC_ADDRESS.wiresharkName})")
append(" payload=${message.payload.size}B")
append(" macRaw=$macRaw macReversed=$macReversed")
if (tailHex.isNotEmpty()) append(" tail=[$tailHex]")
@@ -484,26 +564,56 @@ internal class AapSessionEngine(
if (message.commandType in KNOWN_NON_SETTINGS_COMMANDS) {
_state.value = _state.value.copy(lastMessageAt = timeSource.now())
val namedType = AapMessageType.byValue(message.commandType)
val nameLabel = namedType?.let { " (${it.wiresharkName})" } ?: ""
log(TAG, VERBOSE) {
"Known cmd=0x${"%04X".format(message.commandType)} payload=${message.payload.size}B [$payloadHex] sinceLastSend=${sendInfo.sinceLastSend}ms lastSend=${sendInfo.lastSend}"
"Known cmd=0x${"%04X".format(message.commandType)}$nameLabel payload=${message.payload.size}B [$payloadHex] sinceLastSend=${sendInfo.sinceLastSend}ms lastSend=${sendInfo.lastSend}"
}
return
}
val namedType = AapMessageType.byValue(message.commandType)
val nameLabel = namedType?.let { " (${it.wiresharkName})" } ?: " (unknown)"
log(TAG, INFO) {
"Unhandled cmd=0x${"%04X".format(message.commandType)} payload=${message.payload.size}B [$payloadHex] sinceLastSend=${sendInfo.sinceLastSend}ms lastSend=${sendInfo.lastSend}"
"Unhandled cmd=0x${"%04X".format(message.commandType)}$nameLabel payload=${message.payload.size}B [$payloadHex] sinceLastSend=${sendInfo.sinceLastSend}ms lastSend=${sendInfo.lastSend}"
}
}
companion object {
private val TAG = logTag("AAP", "Engine")
private const val CMD_SETTING = 0x0009
private const val CMD_CONNECTED_DEVICE = 0x000C
private const val CMD_DEVICE_INFO = 0x001D
private const val CMD_HID_DESCRIPTOR = 0x0017
private val KNOWN_NON_SETTINGS_COMMANDS = setOf(
0x0000, 0x0002, 0x002B, 0x004E, 0x0052, 0x0055, 0x0057,
/**
* Opcodes we see on-wire but don't model as a domain update. Decoded only
* enough to refresh `lastMessageAt` (freshness feeds the AAP quality boost
* in PodDevice.computeAapBoost). Add newly-catalogued push-only opcodes here
* when they appear in captures so logs get a named label AND freshness stays
* intact.
*/
private val KNOWN_NON_SETTINGS_COMMANDS: Set<Int> = setOf(
0x0000, // Connect (shouldn't reach here but historically observed)
AapMessageType.CAPABILITIES.value, // 0x0002
AapMessageType.DEVICE_LIST.value, // 0x000B
AapMessageType.TRIANGLE_STATUS_REQUEST.value, // 0x0015
AapMessageType.MAGNET_LINK.value, // 0x0016
AapMessageType.TIMESTAMP.value, // 0x001B
AapMessageType.UNKNOWN_0X21.value, // 0x0021
AapMessageType.CASE_INFO.value, // 0x0023 (handled via decoder later; still refresh)
AapMessageType.GYRO_INFO.value, // 0x0028
AapMessageType.STREAM_STATE_INFO.value, // 0x002B
AapMessageType.GAPA_CHALLENGE.value, // 0x002C
AapMessageType.UNKNOWN_0X40.value, // 0x0040
AapMessageType.ADAPTIVE_VOLUME_MESSAGE.value, // 0x004C
AapMessageType.SOURCE_FEATURE_CAPABILITIES.value, // 0x004D
AapMessageType.FEATURE_PROX_CARD_STATUS_UPDATE.value, // 0x004E
AapMessageType.UARP_DATA.value, // 0x004F
AapMessageType.UNKNOWN_0X50.value, // 0x0050
AapMessageType.SOURCE_CONTEXT.value, // 0x0052
AapMessageType.SET_BAND_EDGES.value, // 0x0054 RF band gating (push-only; see AapMessageType kdoc)
AapMessageType.UNKNOWN_0X55.value, // 0x0055
AapMessageType.SLEEP_DETECTION_UPDATE.value, // 0x0057
AapMessageType.UNKNOWN_0X58.value, // 0x0058
AapMessageType.DYNAMIC_END_OF_CHARGE.value, // 0x0059
AapMessageType.PERSONAL_TRANSLATION.value, // 0x0060
)
}
}
@@ -517,6 +627,7 @@ internal sealed interface AapEngineEvent {
data object HandshakeSent : AapEngineEvent
data object ResetRequested : AapEngineEvent
data class MessageReceived(val message: AapMessage) : AapEngineEvent
data class ConnectResponseReceived(val packet: AapPacket.ConnectResponse) : AapEngineEvent
data class InboundUpdateDecoded(val update: AapInboundUpdate) : AapEngineEvent
data class TimerFired(val key: EngineTimerKey) : AapEngineEvent
}
@@ -172,6 +172,10 @@ internal class AapSettingsCoordinator(
AapSetting.SleepDetection::class to AapSetting.SleepDetection(enabled = command.enabled)
}
is AapCommand.SetDynamicEndOfCharge -> {
AapSetting.DynamicEndOfCharge::class to AapSetting.DynamicEndOfCharge(enabled = command.enabled)
}
is AapCommand.SetDeviceName -> {
val currentInfo = baseState.deviceInfo ?: return null
return baseState.copy(
@@ -205,6 +209,7 @@ internal class AapSettingsCoordinator(
is AapCommand.SetAllowOffOption -> { s -> s.setting<AapSetting.AllowOffOption>()?.enabled == command.enabled }
is AapCommand.SetStemConfig -> { s -> s.setting<AapSetting.StemConfig>()?.claimedPressMask == command.claimedPressMask }
is AapCommand.SetSleepDetection -> { s -> s.setting<AapSetting.SleepDetection>()?.enabled == command.enabled }
is AapCommand.SetDynamicEndOfCharge -> { s -> s.setting<AapSetting.DynamicEndOfCharge>()?.enabled == command.enabled }
is AapCommand.SetDeviceName -> null
}
}
@@ -42,6 +42,12 @@ internal class HidTracker(private val log: (String) -> Unit) {
log("HID: terminator (${type.payloadSize}B)")
}
is HidFrameType.ServiceInfo -> {
flush()
val tokens = type.asciiTokens.joinToString(", ")
log("HID: service info tokens=[$tokens] (${type.payloadSize}B)")
}
is HidFrameType.Other -> {
flush()
val hex = payload.joinToString(" ") { "%02X".format(it) }
@@ -70,10 +76,25 @@ internal class HidTracker(private val log: (String) -> Unit) {
data class ServiceDirectory(val services: List<String>) : HidFrameType()
data class Descriptor(val phase: Int, val fill: Int) : HidFrameType()
data class Terminator(val payloadSize: Int) : HidFrameType()
/**
* 0x0017 "service info" frames — a TLV-ish metadata dump that carries
* ASCII key/value pairs like `VendorID`, `SerialNumber`, `CFG`,
* `ReportDescriptor`, etc.
*
* We don't decode the TLV structure yet (the framing is not fully
* documented). Instead we extract all printable ASCII runs of length
* ≥ 3 so the log tells you which keys/values the frame contains.
*/
data class ServiceInfo(val asciiTokens: List<String>, val payloadSize: Int) : HidFrameType()
data object Other : HidFrameType()
}
companion object {
/** Minimum length for an ASCII run to count as a token in a ServiceInfo frame. */
private const val MIN_ASCII_TOKEN_LEN = 3
internal fun classify(payload: ByteArray): HidFrameType {
// Service directory frame — starts with FE 00 00 and contains repeated
// [len=4? ascii service name + 4B flags] blocks. For logging, extract names.
@@ -111,7 +132,40 @@ internal class HidTracker(private val log: (String) -> Unit) {
return HidFrameType.Terminator(payload.size)
}
// "Service info" frame — 4-byte magic 00 00 10 00 followed by a TLV-ish
// payload with ASCII keys and mixed binary values. Observed on AirPods Pro 2
// USB-C after the descriptor batch.
if (payload.size >= 8 &&
payload[0] == 0x00.toByte() &&
payload[1] == 0x00.toByte() &&
payload[2] == 0x10.toByte() &&
payload[3] == 0x00.toByte()
) {
return HidFrameType.ServiceInfo(
asciiTokens = extractAsciiTokens(payload),
payloadSize = payload.size,
)
}
return HidFrameType.Other
}
/** Extract printable ASCII runs ≥ 3 chars. Skips all non-printable bytes. */
private fun extractAsciiTokens(payload: ByteArray): List<String> {
val tokens = mutableListOf<String>()
var i = 0
while (i < payload.size) {
val startByte = payload[i].toInt() and 0xFF
if (startByte in 0x20..0x7E) {
val start = i
while (i < payload.size && (payload[i].toInt() and 0xFF) in 0x20..0x7E) i++
val run = String(payload, start, i - start, Charsets.US_ASCII)
if (run.length >= MIN_ASCII_TOKEN_LEN) tokens += run
} else {
i++
}
}
return tokens
}
}
}
@@ -0,0 +1,54 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* Information about the case itself (message type 0x23, requested via 0x22).
*
* Fields defined by the Wireshark AAP dissector:
* - CaseInfoMessageVersion
* - CaseInfoVID / CaseInfoPID / CaseInfoVIDSource
* - CaseInfoColor
* - CaseInfoVersion (case firmware)
* - CaseInfoName
*
* The Wireshark dissector does not yet decode the per-field byte offsets
* reliably — the payload length and presence of each field varies across
* models. This class holds the raw bytes for future iteration once more
* captures are available, and a best-effort named-field view.
*/
data class AapCaseInfo(
/** The complete payload (after the 6-byte header) for future analysis. */
val rawPayload: ByteArray,
val messageVersion: Int? = null,
val vid: Int? = null,
val pid: Int? = null,
val vidSource: Int? = null,
val color: Int? = null,
val version: String? = null,
val name: String? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AapCaseInfo) return false
if (!rawPayload.contentEquals(other.rawPayload)) return false
if (messageVersion != other.messageVersion) return false
if (vid != other.vid) return false
if (pid != other.pid) return false
if (vidSource != other.vidSource) return false
if (color != other.color) return false
if (version != other.version) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var r = rawPayload.contentHashCode()
r = 31 * r + (messageVersion ?: 0)
r = 31 * r + (vid ?: 0)
r = 31 * r + (pid ?: 0)
r = 31 * r + (vidSource ?: 0)
r = 31 * r + (color ?: 0)
r = 31 * r + (version?.hashCode() ?: 0)
r = 31 * r + (name?.hashCode() ?: 0)
return r
}
}
@@ -34,5 +34,6 @@ sealed class AapCommand {
data class SetAllowOffOption(val enabled: Boolean) : AapCommand()
data class SetStemConfig(val claimedPressMask: Int) : AapCommand()
data class SetSleepDetection(val enabled: Boolean) : AapCommand()
data class SetDynamicEndOfCharge(val enabled: Boolean) : AapCommand()
data class SetDeviceName(val name: String) : AapCommand()
}
@@ -0,0 +1,134 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* Catalog of AAP control/setting IDs — the 8-bit ID at payload byte 0 of a
* Control message (see [AapMessageType.CONTROL], opcode 0x0009).
*
* Sources:
* - https://github.com/pabloaul/apple-wireshark/blob/main/plugins/aacp.lua
* - LibrePods / MagicPodsCore
* - Real device captures (AirPods Pro 1, Pro 2 USB-C, Pro 3)
*
* Where CAPod ships a setting under a different user-facing name than the
* Wireshark catalog uses, both names are preserved — the enum uses the
* Wireshark name, UI-facing strings use the CAPod name.
*/
enum class AapControlId(val value: Int, val wiresharkName: String) {
MIC_MODE(0x01, "Mic Mode"),
SCAN(0x02, "Scan"),
RESET(0x03, "Reset"),
BASIC_DOUBLE_TAP_MODE(0x04, "Basic Double Tap Mode"),
BUTTON_SEND_MODE(0x05, "Button Send Mode"),
OWNERSHIP_STATE(0x06, "Ownership state"),
TAP_INTERVAL(0x07, "Tap Interval"),
/** Request the connected bud to go secondary. */
BUD_ROLE(0x08, "Bud Role"),
DEBUG_GET_DATA(0x09, "Debug Get Data"),
IN_EAR_DETECTION(0x0A, "In Ear Detection"),
/** Aka "Dynamic Latency". */
JITTER_BUFFER(0x0B, "Jitter Buffer"),
DOUBLE_TAP_MODE(0x0C, "Double Tap Mode"),
LISTEN_MODE(0x0D, "Listen Mode"),
HEART_RATE_MONITOR_1(0x0E, "Heart Rate Monitor"),
HEART_RATE_MONITOR_2(0x0F, "Heart Rate Monitor"),
UNKNOWN_0X10(0x10, "Unknown/Unassigned"),
SWITCH_CONTROL(0x11, "Switch Control"),
VOICE_TRIGGER(0x12, "Voice Trigger"),
/** "Dictation over AirPods" for Siri. */
DOAP_MODE(0x13, "DoAP mode"),
SINGLE_CLICK(0x14, "Single Click"),
DOUBLE_CLICK(0x15, "Double Click"),
CLICK_AND_HOLD(0x16, "Click and Hold"),
/** CAPod ships this as "Press Speed". */
DOUBLE_CLICK_INTERVAL(0x17, "Double Click Interval"),
/** CAPod ships this as "Press Hold Duration". */
CLICK_AND_HOLD_INTERVAL(0x18, "Click and Hold Interval"),
UNKNOWN_0X19(0x19, "Unknown/Unassigned"),
LISTENING_MODE_CONFIGS(0x1A, "Listening Mode Configs"),
ONE_BUD_ANC_MODE(0x1B, "One Bud ANC Mode"),
CROWN_ROTATION_DIRECTION(0x1C, "Crown Rotation Direction"),
UNKNOWN_0X1D(0x1D, "Unknown/Unassigned"),
AUTO_ANSWER_MODE(0x1E, "Auto Answer Mode"),
/** CAPod ships this as "Tone Volume". */
CHIME_VOLUME(0x1F, "Chime Volume"),
SMART_ROUTING_MODE(0x20, "Smart Routing Mode"),
UNKNOWN_0X21(0x21, "Unknown/Unassigned"),
HFP_UPLINK_MODE(0x22, "HFP Uplink Mode"),
/** CAPod ships this as "Volume Swipe Length". */
VOLUME_SWIPE_INTERVAL(0x23, "Volume Swipe Interval"),
/** CAPod ships this as "End Call / Mute Mic". */
CALL_MANAGEMENT_CONFIG(0x24, "Call Management Config"),
/** CAPod ships this as "Volume Swipe". */
VOLUME_SWIPE_MODE(0x25, "Volume Swipe Mode"),
/**
* "Adaptive Volume" per Wireshark. CAPod ships it as "Personalized Volume"
* because that matches the iOS Settings.app label — unclear if this is
* the same feature or the two sources disagree on naming.
*/
ADAPTIVE_VOLUME(0x26, "Adaptive Volume"),
SOFTWARE_MUTE(0x27, "Software Mute"),
/** CAPod ships this as "Conversational Awareness". */
CONVERSATION_DETECT(0x28, "Conversation Detect"),
SELECTIVE_SPEECH_LISTENING(0x29, "Selective Speech Listening"),
UNKNOWN_0X2A(0x2A, "Unknown/Unassigned"),
UNKNOWN_0X2B(0x2B, "Unknown/Unassigned"),
HEARING_AID(0x2C, "Hearing Aid"),
UNKNOWN_0X2D(0x2D, "Unknown/Unassigned"),
/** CAPod ships this as "Adaptive Audio Noise". */
AUTO_ANC_STRENGTH(0x2E, "Auto ANC Strength"),
HEARING_AID_GAIN_SWIPE(0x2F, "Hearing Aid Gain Swipe"),
HEART_RATE_MONITOR_3(0x30, "Heart Rate Monitor"),
IN_CASE_TONE(0x31, "In-Case Tone"),
SIRI_MULTITONE(0x32, "Siri Multitone"),
HEARING_ASSIST(0x33, "Hearing Assist"),
ALLOW_OFF_OPTION(0x34, "Allow Off Option"),
SLEEP_DETECTION(0x35, "Sleep Detection"),
ALLOW_AUTO_CONNECT_FROM_AUDIO_ACCESSORY(0x36, "Allow Auto Connect from Audio Accessory"),
HEARING_PROTECTION_PPE(0x37, "Hearing Protection PPE"),
PPE_CAP_LEVEL_CONFIG(0x38, "PPE Cap Level Config"),
/** CAPod ships this as "Stem Config". */
RAW_GESTURES_CONFIG(0x39, "Raw Gestures Config"),
ALLOW_TEMPORARY_MANAGED_PAIRING(0x3A, "Allow Temporary Managed Pairing"),
DYNAMIC_END_OF_CHARGE(0x3B, "Dynamic End of Charge"),
SYSTEM_SIRI_MODE(0x3C, "System Siri Mode"),
/** "hearingAidV2SourceRegionSupport" per Wireshark. */
HEARING_AID_GENERIC(0x3D, "Hearing Aid Generic"),
UPLINK_EQ_BUD(0x3E, "Uplink EQ Bud"),
UPLINK_EQ_SOURCE(0x3F, "Uplink EQ Source"),
/** Separate from [IN_CASE_TONE] (0x31) — this is a volume level, not an on/off. */
IN_CASE_TONE_VOLUME(0x40, "In Case Tone Volume"),
DISABLE_BUTTON_INPUT(0x41, "Disable Button Input"),
EXTENDED_HOLD_AND_RELEASE(0x42, "Extended Hold and Release"),
;
companion object {
private val byValue: Map<Int, AapControlId> = entries.associateBy { it.value }
fun byValue(value: Int): AapControlId? = byValue[value]
}
}
@@ -1,15 +1,96 @@
package eu.darken.capod.pods.core.apple.aap.protocol
import java.time.Instant
/**
* Device identity parsed from the AAP handshake response (message type 0x1D).
* Device identity parsed from the AAP Information message (type 0x1D).
*
* Field ordering and semantics are sourced from the Wireshark AAP dissector:
* <https://github.com/pabloaul/apple-wireshark/blob/main/plugins/aacp.lua>
*
* Segments 0-10 are NUL-delimited UTF-8. Segments 11-12 are fixed 17-byte
* UUIDs (may contain 0x00 — never treat as strings). Segments 13-14 are
* NUL-delimited timestamps (Unix epoch seconds, ASCII).
*
* Any field past the system fields may be absent on older devices or
* truncated payloads — all optional fields are nullable.
*/
data class AapDeviceInfo(
/** Segment 0 — user-visible device name ("AirPods Pro"). */
val name: String,
/** Segment 1 — Apple model identifier ("A2084"). */
val modelNumber: String,
/** Segment 2 — manufacturer string ("Apple Inc."). */
val manufacturer: String,
/** Segment 3 — system (case) serial number. */
val serialNumber: String,
/** Segment 4 — currently-running firmware version. */
val firmwareVersion: String,
/** Segment 5 — pending firmware version after next reboot. Null if equal to active. */
val firmwareVersionPending: String? = null,
/** Segment 6 — hardware revision ("1.0.0"). */
val hardwareVersion: String? = null,
/** Segment 7 — External Accessory protocol name ("com.apple.accessory.updater.app.71"). */
val eaProtocolName: String? = null,
/** Segment 8 — left earbud serial. */
val leftEarbudSerial: String? = null,
/** Segment 9 — right earbud serial. */
val rightEarbudSerial: String? = null,
val buildNumber: String? = null,
)
/**
* Segment 10 — marketing/build version (e.g. "8454624"). Originally
* mis-labeled `buildNumber` in CAPod; the Wireshark dissector calls it
* "Marketing Version".
*/
val marketingVersion: String? = null,
/** Segment 11 — opaque 17-byte left-bud UUID. May contain arbitrary bytes. */
val leftEarbudUuid: ByteArray? = null,
/** Segment 12 — opaque 17-byte right-bud UUID. May contain arbitrary bytes. */
val rightEarbudUuid: ByteArray? = null,
/** Segment 13 — first-time-pairing timestamp for the left bud. */
val leftEarbudFirstPaired: Instant? = null,
/** Segment 14 — first-time-pairing timestamp for the right bud. */
val rightEarbudFirstPaired: Instant? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AapDeviceInfo) return false
if (name != other.name) return false
if (modelNumber != other.modelNumber) return false
if (manufacturer != other.manufacturer) return false
if (serialNumber != other.serialNumber) return false
if (firmwareVersion != other.firmwareVersion) return false
if (firmwareVersionPending != other.firmwareVersionPending) return false
if (hardwareVersion != other.hardwareVersion) return false
if (eaProtocolName != other.eaProtocolName) return false
if (leftEarbudSerial != other.leftEarbudSerial) return false
if (rightEarbudSerial != other.rightEarbudSerial) return false
if (marketingVersion != other.marketingVersion) return false
if (!leftEarbudUuid.contentOptionalEquals(other.leftEarbudUuid)) return false
if (!rightEarbudUuid.contentOptionalEquals(other.rightEarbudUuid)) return false
if (leftEarbudFirstPaired != other.leftEarbudFirstPaired) return false
if (rightEarbudFirstPaired != other.rightEarbudFirstPaired) return false
return true
}
override fun hashCode(): Int {
var r = name.hashCode()
r = 31 * r + modelNumber.hashCode()
r = 31 * r + manufacturer.hashCode()
r = 31 * r + serialNumber.hashCode()
r = 31 * r + firmwareVersion.hashCode()
r = 31 * r + (firmwareVersionPending?.hashCode() ?: 0)
r = 31 * r + (hardwareVersion?.hashCode() ?: 0)
r = 31 * r + (eaProtocolName?.hashCode() ?: 0)
r = 31 * r + (leftEarbudSerial?.hashCode() ?: 0)
r = 31 * r + (rightEarbudSerial?.hashCode() ?: 0)
r = 31 * r + (marketingVersion?.hashCode() ?: 0)
r = 31 * r + (leftEarbudUuid?.contentHashCode() ?: 0)
r = 31 * r + (rightEarbudUuid?.contentHashCode() ?: 0)
r = 31 * r + (leftEarbudFirstPaired?.hashCode() ?: 0)
r = 31 * r + (rightEarbudFirstPaired?.hashCode() ?: 0)
return r
}
}
private fun ByteArray?.contentOptionalEquals(other: ByteArray?): Boolean =
if (this == null || other == null) this === other else contentEquals(other)
@@ -74,6 +74,33 @@ interface AapDeviceProfile {
*/
fun decodeStemPress(message: AapMessage): StemPressEvent?
/**
* Encode a Case Info request (command 0x22). Returns null when this profile
* shouldn't probe the case — older models may not respond, or the payload
* schema is unconfirmed for that model. Fire-and-forget: if the device
* doesn't reply, nothing happens.
*/
fun encodeCaseInfoRequest(): ByteArray? = null
/**
* Decode a Case Info response (command 0x23). Returns null if the message
* is not a Case Info response or the payload cannot be recognised.
*/
fun decodeCaseInfo(message: AapMessage): AapCaseInfo? = null
/**
* Decode a Sleep Detection Update event (command 0x57). Returns null for
* any other message type. Payload schema is not fully documented — the
* default implementation returns the raw bytes verbatim.
*/
fun decodeSleepEvent(message: AapMessage): AapSleepEvent? = null
/**
* Decode a Dynamic End-of-Charge event (command 0x59). Returns null for
* any other message type.
*/
fun decodeDynamicEndOfChargeEvent(message: AapMessage): AapDynamicEndOfChargeEvent? = null
companion object {
fun forModel(model: PodModel): AapDeviceProfile = DefaultAapDeviceProfile(model)
}
@@ -0,0 +1,20 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* Push-only event paired with the Dynamic End-of-Charge setting (control ID 0x3B).
* Message type 0x59 — reports charge-cap status transitions (e.g. "80% cap reached").
*
* Payload schema is not yet documented publicly — this type preserves the raw
* bytes so future capture-based analysis can fill in structured fields.
*/
data class AapDynamicEndOfChargeEvent(
val rawPayload: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AapDynamicEndOfChargeEvent) return false
return rawPayload.contentEquals(other.rawPayload)
}
override fun hashCode(): Int = rawPayload.contentHashCode()
}
@@ -1,36 +1,5 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* A parsed AAP protocol message.
*/
data class AapMessage(
val raw: ByteArray,
val commandType: Int,
val payload: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AapMessage) return false
return raw.contentEquals(other.raw)
}
override fun hashCode(): Int = raw.contentHashCode()
companion object {
/**
* Parse a complete AAP message from raw bytes.
* AAP messages have the format: [4-byte header] [2-byte command type] [payload]
* Minimum message size is 6 bytes (header + command type with no payload).
*/
fun parse(raw: ByteArray): AapMessage? {
if (raw.size < 6) return null
val commandType = (raw[4].toInt() and 0xFF) or ((raw[5].toInt() and 0xFF) shl 8)
val payload = if (raw.size > 6) raw.copyOfRange(6, raw.size) else ByteArray(0)
return AapMessage(raw = raw.copyOf(), commandType = commandType, payload = payload)
}
}
}
/**
* Accumulates bytes from a stream and emits complete [AapMessage] objects.
*
@@ -39,6 +8,12 @@ data class AapMessage(
*
* AAP message framing: first 4 bytes are header, bytes 2-3 (little-endian)
* indicate total message length (excluding the first 4 header bytes).
*
* Note: production code bypasses this framer [AapConnection.readLoop]
* parses whole reads directly because L2CAP SEQPACKET already delivers
* per-frame boundaries. The framer is retained for future stream-mode
* paths but its splitting rule doesn't match every real capture
* (see `AapFramerTest` for the shape it expects).
*/
class AapFramer {
@@ -0,0 +1,124 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* Catalog of AAP (Apple Accessory Protocol, also "AACP") message types —
* the 16-bit opcode at bytes 4-5 of a Message-type packet (packet type 0x04).
*
* Sources:
* - https://github.com/pabloaul/apple-wireshark/blob/main/plugins/aacp.lua
* - LibrePods / MagicPodsCore
* - Real device captures (AirPods Pro 1, Pro 2 USB-C, Pro 3)
*
* Entries whose names start with `UNKNOWN_` exist in Wireshark's dissector
* but have no known meaning. They're catalogued here so unhandled-message
* logging still reports a named opcode instead of a bare int.
*/
enum class AapMessageType(val value: Int, val wiresharkName: String) {
CAPABILITIES_REQUEST(0x0001, "Capabilities Request"),
CAPABILITIES(0x0002, "Capabilities"),
BATTERY_INFO_REQUEST(0x0003, "Battery Info Request"),
BATTERY_INFO(0x0004, "Battery Info"),
EAR_DETECTION_REQUEST(0x0005, "Ear Detection Request"),
EAR_DETECTION(0x0006, "Ear Detection"),
BUD_ROLE_REQUEST(0x0007, "Bud Role Request"),
BUD_ROLE(0x0008, "Bud Role"),
/** Controls / settings. The control ID is payload byte 0 — see [AapControlId]. */
CONTROL(0x0009, "Control"),
DEVICE_LIST(0x000B, "Device List"),
MAC_ADDRESS(0x000C, "MAC Address"),
STREAM_STATE_INFO_REQUEST(0x000D, "Stream State Info Request"),
AUDIO_SOURCE(0x000E, "Audio Source"),
SET_NOTIFICATION_FILTER(0x000F, "Set Notification Filter"),
SMART_ROUTING_1(0x0010, "Smart Routing"),
SMART_ROUTING_2(0x0011, "Smart Routing"),
EASY_PAIR_REQUEST(0x0012, "Easy Pair Request?"),
CONNECT_PRIORITY_LIST(0x0014, "Connect Priority List"),
TRIANGLE_STATUS_REQUEST(0x0015, "Triangle Status Request"),
MAGNET_LINK(0x0016, "Magnet Link"),
/**
* Buddy Command per Wireshark. In CAPod we treat payloads of this opcode as
* HID descriptor frames (Service Directory / descriptor / terminator — see [HidTracker]).
* Both interpretations may be correct for different sub-payloads.
*/
BUDDY_COMMAND(0x0017, "BuddyCommand"),
STEM_PRESS(0x0019, "Stem Press"),
RENAME(0x001A, "Rename"),
TIMESTAMP(0x001B, "Timestamp"),
INFORMATION(0x001D, "Information"),
SEND_EXTERNAL_ACCESSORY_SESSION_PACKET(0x001E, "Send External Accessory Session Packet"),
NOTIFY_SESSION_STATE(0x001F, "Notify Session State?"),
SEND_REMOTE_FIRMWARE_AUTH_DATA(0x0020, "Send Remote Firmware Auth Data"),
UNKNOWN_0X21(0x0021, "Unknown"),
CASE_INFO_REQUEST(0x0022, "Case Info Request"),
CASE_INFO(0x0023, "Case Info"),
SEND_DEVICE_INFO(0x0024, "Send Device Info?"),
CERTIFICATES_REQUEST(0x0026, "Certificates Request"),
CERTIFICATES(0x0027, "Certificates"),
GYRO_INFO(0x0028, "Gyro Info"),
SET_COUNTRY_CODE(0x0029, "Set Country Code"),
STREAM_STATE_INFO(0x002B, "Stream State Info"),
GAPA_CHALLENGE(0x002C, "GAPA Challenge"),
CONNECTED_DEVICES_REQUEST(0x002D, "Connected Devices Request"),
CONNECTED_DEVICES(0x002E, "Connected Devices"),
MAGIC_KEYS_REQUEST(0x0030, "Magic Keys Request"),
MAGIC_KEYS(0x0031, "Magic Keys"),
MAGIC_KEYS_2(0x0032, "Magic Keys"),
UNKNOWN_0X40(0x0040, "Unknown"),
SEND_SMART_ROUTING_2_INFO(0x0044, "Send Smart Routing 2.0 Info"),
FAST_CONNECT_COMPLETE(0x0045, "Fast Connect Complete?"),
BUD_SWAP_2_PROCEDURE(0x0047, "Bud Swap 2.0 Procedure?"),
SWAP_IMMINENT_CONFIRM(0x0048, "Swap Imminent Confirm?"),
BUD_SWAP_2_COMPLETION(0x0049, "Bud Swap 2.0 Completion?"),
SWAP_COMPLETE_CONFIRM(0x004A, "Swap Complete Confirm?"),
CONVERSATIONAL_AWARENESS(0x004B, "Conversational Awareness"),
ADAPTIVE_VOLUME_MESSAGE(0x004C, "Adaptive Volume Message"),
/**
* Source Feature Capabilities. Sent by the source after Connect Response to
* advertise what it supports. In CAPod this is "InitExt" — we only emit a
* fixed template, we don't read/decode the reply.
*/
SOURCE_FEATURE_CAPABILITIES(0x004D, "Source Feature Capabilities"),
FEATURE_PROX_CARD_STATUS_UPDATE(0x004E, "Feature ProxCard Status Update"),
UARP_DATA(0x004F, "UARP Data"),
UNKNOWN_0X50(0x0050, "Unknown"),
SOURCE_CONTEXT(0x0052, "Source Context"),
/**
* PME = Personal Medical Equipment (cf. PPE = Personal Protective Equipment) —
* hearing-aid configuration for the iOS 18.1+ hearing-aid feature on AirPods
* Pro 2. Decoded as 4 × 8 Float32 values (see [AapSetting.PmeConfig]); see
* that data class for the layout rationale. "PME Config" is the label the
* Wireshark AAP dissector uses for this opcode.
*/
PME_CONFIG(0x0053, "PME Config"),
/**
* Configures the AirPods radio's allowed RF bands. AirPods Pro 2 USB-C
* and newer can transmit on 5 GHz / 6 GHz U-NII bands in addition to
* 2.4 GHz ISM — Apple uses this for the proprietary lossless audio mode
* with Apple Vision Pro. The "type" field is a band code from Apple's
* `BSM_BAND_CODE_*` enum (0x0 = ISM24, 0x1/0x2/0x3 = U-NII-1/3/4,
* 0x40x7 = U-NII-5A/B/C/D, 0x8 = INVALID; identified via FCC firmware
* analysis). "Set Band Edges" is the label the Wireshark AAP dissector
* uses for this opcode. CAPod observes but does not decode the payload.
*/
SET_BAND_EDGES(0x0054, "Set Band Edges"),
UNKNOWN_0X55(0x0055, "Unknown"),
USB_SPATIAL_SENSOR_DATA_REQUEST(0x0056, "USB Spatial Sensor Data Request"),
SLEEP_DETECTION_UPDATE(0x0057, "Sleep Detection Update"),
UNKNOWN_0X58(0x0058, "Unknown"),
DYNAMIC_END_OF_CHARGE(0x0059, "Dynamic End Of Charge"),
PERSONAL_TRANSLATION(0x0060, "Personal Translation"),
;
companion object {
private val byValue: Map<Int, AapMessageType> = entries.associateBy { it.value }
fun byValue(value: Int): AapMessageType? = byValue[value]
}
}
@@ -0,0 +1,172 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* A parsed AAP (AACP) frame. Packet type lives at bytes 0-1 (little-endian)
* and selects which variant we're looking at.
*
* Per the Wireshark dissector, packet types are:
* - 0x0000 — Connect (session open request; source → pods)
* - 0x0001 — Connect Response (pods → source)
* - 0x0002 — Disconnect
* - 0x0003 — Disconnect Response
* - 0x0004 — Message (the large Message type with a 16-bit message/command ID)
*
* Only `Message` packets carry the `(commandType, payload)` pair that the
* existing decoder pipeline consumes; the other variants have their own
* field schema and bypass the decoder.
*/
sealed class AapPacket(val raw: ByteArray) {
/** Source → pods session open. We emit this as our handshake. */
class Connect(
raw: ByteArray,
val service: Int,
val major: Int,
val minor: Int,
val features: ULong,
) : AapPacket(raw)
/**
* Pods → source response to a Connect. The `features` bitmask is opaque
* for now (no public bit-to-feature mapping); CAPod logs it and stores
* it in `AapPodState` for future correlation work.
*/
class ConnectResponse(
raw: ByteArray,
val service: Int,
val status: Int,
val major: Int,
val minor: Int,
val features: ULong,
) : AapPacket(raw)
class Disconnect(
raw: ByteArray,
val service: Int,
val status: Int,
) : AapPacket(raw)
class DisconnectResponse(
raw: ByteArray,
val service: Int,
) : AapPacket(raw)
/**
* Message packet — bytes 4-5 are the message/command ID. This is what
* [AapDeviceProfile.decodeSetting] / `decodeBattery` / etc. consume.
*
* Typealiased to `AapMessage` for backward compatibility.
*/
class Message(
raw: ByteArray,
val commandType: Int,
val payload: ByteArray,
) : AapPacket(raw) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Message) return false
return raw.contentEquals(other.raw)
}
override fun hashCode(): Int = raw.contentHashCode()
override fun toString(): String =
"AapPacket.Message(cmd=0x${"%04X".format(commandType)}, payload=${payload.size}B)"
companion object {
/**
* Parse a Message-type AAP frame. Returns null if the bytes aren't
* a complete Message packet (packet type != 0x0004, or payload too short).
* Non-Message packets (Connect Response etc.) return null here —
* use [AapPacket.parse] if you need to handle them.
*/
fun parse(raw: ByteArray): Message? {
val packet = AapPacket.parse(raw) ?: return null
return packet as? Message
}
}
}
/** Unknown / unrecognised packet type. Preserved for logging. */
class Unknown(raw: ByteArray, val packetType: Int) : AapPacket(raw)
companion object {
private const val PACKET_TYPE_CONNECT = 0x0000
private const val PACKET_TYPE_CONNECT_RESPONSE = 0x0001
private const val PACKET_TYPE_DISCONNECT = 0x0002
private const val PACKET_TYPE_DISCONNECT_RESPONSE = 0x0003
private const val PACKET_TYPE_MESSAGE = 0x0004
/**
* Parse a raw AAP frame. Returns:
* - `null` if the buffer is too short to even read the packet type.
* - An [Unknown] if the packet type isn't one we recognise.
* - The appropriate subclass otherwise.
*
* Note: this assumes one frame per call — L2CAP SEQPACKET delivers
* complete frames per read, matching this expectation. The
* (unused-in-prod) [AapFramer] uses a different splitting approach.
*/
fun parse(raw: ByteArray): AapPacket? {
if (raw.size < 4) return null
val packetType = readLe16(raw, 0)
val service = readLe16(raw, 2)
return when (packetType) {
PACKET_TYPE_CONNECT -> parseConnect(raw, service)
PACKET_TYPE_CONNECT_RESPONSE -> parseConnectResponse(raw, service)
PACKET_TYPE_DISCONNECT -> parseDisconnect(raw, service)
PACKET_TYPE_DISCONNECT_RESPONSE -> DisconnectResponse(raw.copyOf(), service)
PACKET_TYPE_MESSAGE -> parseMessage(raw)
else -> Unknown(raw.copyOf(), packetType)
}
}
private fun parseConnect(raw: ByteArray, service: Int): Connect? {
if (raw.size < 16) return null
val major = readLe16(raw, 4)
val minor = readLe16(raw, 6)
val features = readLe64(raw, 8)
return Connect(raw.copyOf(), service, major, minor, features)
}
private fun parseConnectResponse(raw: ByteArray, service: Int): ConnectResponse? {
if (raw.size < 18) return null
val status = readLe16(raw, 4)
val major = readLe16(raw, 6)
val minor = readLe16(raw, 8)
val features = readLe64(raw, 10)
return ConnectResponse(raw.copyOf(), service, status, major, minor, features)
}
private fun parseDisconnect(raw: ByteArray, service: Int): Disconnect? {
if (raw.size < 6) return null
val status = readLe16(raw, 4)
return Disconnect(raw.copyOf(), service, status)
}
private fun parseMessage(raw: ByteArray): Message? {
if (raw.size < 6) return null
val commandType = readLe16(raw, 4)
val payload = if (raw.size > 6) raw.copyOfRange(6, raw.size) else ByteArray(0)
return Message(raw = raw.copyOf(), commandType = commandType, payload = payload)
}
private fun readLe16(data: ByteArray, offset: Int): Int =
(data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
private fun readLe64(data: ByteArray, offset: Int): ULong {
var result = 0UL
for (i in 0 until 8) {
result = result or ((data[offset + i].toLong() and 0xFFL).toULong() shl (i * 8))
}
return result
}
}
}
/**
* Backward-compat alias — CAPod historically named the Message packet just
* `AapMessage`. Kept as a type alias so decoder signatures don't churn.
*/
typealias AapMessage = AapPacket.Message
@@ -146,6 +146,15 @@ sealed class AapSetting {
val enabled: Boolean,
) : AapSetting()
/**
* Apple's "Optimized Charge Limit" — AAP setting 0x3B, Apple-bool encoding. Distinct
* from the older "Optimized Battery Charging" which Apple doesn't expose as a
* user-controllable AAP setting. Supported models push this value on connect.
*/
data class DynamicEndOfCharge(
val enabled: Boolean,
) : AapSetting()
data class InCaseTone(
val enabled: Boolean,
) : AapSetting()
@@ -163,9 +172,26 @@ sealed class AapSetting {
enum class AudioSourceType { NONE, CALL, MEDIA }
}
data class EqBands(
/**
* Payload of message type 0x0053 — "PME Config" in the Wireshark AAP dissector.
* PME = Personal Medical Equipment (cf. PPE = Personal Protective Equipment):
* the hearing-aid configuration for Apple's iOS 18.1+ hearing-aid feature on
* AirPods Pro 2.
*
* Decoded as 4 × 8 Float32 values — consistent with per-ear × per-profile
* audiogram band gains (e.g. L/R × two environment profiles, 8 frequency
* bands). CAPod previously called this "EQ bands".
*
* Callers should treat all-zero [sets] as "no hearing-aid profile configured"
* — stock firmware reports zeros until the user runs Apple's Hearing Test /
* hearing-aid setup.
*/
data class PmeConfig(
val sets: List<List<Float>>,
) : AapSetting()
) : AapSetting() {
val isAllZero: Boolean
get() = sets.all { set -> set.all { it == 0f } }
}
/** Per-pod placement reported by the device (command 0x06). */
data class EarDetection(
@@ -0,0 +1,20 @@
package eu.darken.capod.pods.core.apple.aap.protocol
/**
* Push-only event emitted by newer AirPods firmware when the Sleep Detection
* feature (setting 0x35) is enabled (message type 0x57 "Sleep Detection Update").
*
* Payload schema is not yet documented publicly — this type preserves the raw
* bytes so future capture-based analysis can fill in structured fields.
*/
data class AapSleepEvent(
val rawPayload: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AapSleepEvent) return false
return rawPayload.contentEquals(other.rawPayload)
}
override fun hashCode(): Int = rawPayload.contentHashCode()
}
@@ -16,58 +16,42 @@ class DefaultAapDeviceProfile(
) : AapDeviceProfile {
companion object {
// AAP command types (bytes 4-5 of the message, little-endian)
const val CMD_SETTINGS = 0x0009
const val CMD_BATTERY = 0x0004
const val CMD_DEVICE_INFO = 0x001D
const val CMD_PRIVATE_KEYS_RESPONSE = 0x0031
const val CMD_EAR_DETECTION = 0x0006
const val CMD_PRIMARY_POD = 0x0008
const val CMD_CONVERSATION_AWARENESS_STATE = 0x004B
// Setting IDs (first byte of settings command payload)
const val SETTING_ANC_MODE = 0x0D
const val SETTING_PRESS_SPEED = 0x17
const val SETTING_PRESS_HOLD_DURATION = 0x18
const val SETTING_NC_ONE_AIRPOD = 0x1B
const val SETTING_TONE_VOLUME = 0x1F
const val SETTING_VOLUME_SWIPE_LENGTH = 0x23
const val SETTING_END_CALL_MUTE_MIC = 0x24
const val SETTING_VOLUME_SWIPE = 0x25
const val SETTING_PERSONALIZED_VOLUME = 0x26
const val SETTING_CONVERSATIONAL_AWARENESS = 0x28
const val SETTING_ADAPTIVE_AUDIO_NOISE = 0x2E
const val SETTING_MICROPHONE_MODE = 0x01
const val SETTING_EAR_DETECTION_ENABLED = 0x0A
const val SETTING_LISTENING_MODE_CYCLE = 0x1A
// Kept decoded internally (never exposed in UI): the device pushes 0x31 frames and dropping the
// decode would fall through to "Unhandled message" logging and prevent lastMessageAt refresh,
// which AAP freshness / boost logic in PodDevice.computeAapBoost depends on.
// Originally labeled "Charging Sounds" but the real case tones are controlled over ATT, not here;
// the actual effect of this setting is unknown, so we don't expose or write it.
const val SETTING_IN_CASE_TONE = 0x31
const val SETTING_ALLOW_OFF_OPTION = 0x34
const val SETTING_SLEEP_DETECTION = 0x35
const val SETTING_STEM_CONFIG = 0x39
// Known-but-unconfirmed setting IDs (H2+ exclusive). Decoded as UnknownSetting
// to keep lastMessageAt fresh. Observed on Pro 2 USB-C and/or Pro 3.
val UNCONFIRMED_SETTING_IDS = setOf(
0x29, 0x2C, 0x2F, 0x30, 0x33, 0x37, 0x38, 0x3B, 0x3E,
/**
* Catalogued control IDs we see on-wire but don't yet model as a named
* [AapSetting] subclass. Decoded as [AapSetting.UnknownSetting] so
* `lastMessageAt` still refreshes (freshness feeds AAP quality boost).
* Promote an entry to a dedicated subclass when its semantics are
* confirmed from captures.
*
* Observed on Pro 2 USB-C and/or Pro 3.
*/
val UNMAPPED_SETTING_IDS = setOf(
AapControlId.SELECTIVE_SPEECH_LISTENING.value, // 0x29
AapControlId.HEARING_AID.value, // 0x2C
AapControlId.HEARING_AID_GAIN_SWIPE.value, // 0x2F
AapControlId.HEART_RATE_MONITOR_3.value, // 0x30
AapControlId.HEARING_ASSIST.value, // 0x33
AapControlId.HEARING_PROTECTION_PPE.value, // 0x37
AapControlId.PPE_CAP_LEVEL_CONFIG.value, // 0x38
AapControlId.UPLINK_EQ_BUD.value, // 0x3E
)
// Command types for non-settings messages
const val CMD_RENAME = 0x001E
const val CMD_STEM_PRESS = 0x0019
const val CMD_CONNECTED_DEVICES = 0x002E
const val CMD_AUDIO_SOURCE = 0x000E
const val CMD_EQ_DATA = 0x0053
// ANC mode wire values
const val ANC_WIRE_OFF = 0x01
const val ANC_WIRE_ON = 0x02
const val ANC_WIRE_TRANSPARENCY = 0x03
const val ANC_WIRE_ADAPTIVE = 0x04
/** Fixed length of each earbud UUID segment in the Information payload (segments 11 & 12). */
private const val UUID_LEN = 17
/**
* Models where sending a 0x22 CASE_INFO_REQUEST yields a 0x23 response.
* Grow this list as captures confirm other models respond correctly.
*/
private val CASE_INFO_ALLOWLIST = setOf(
PodModel.AIRPODS_PRO3,
)
}
private val supportedAncModes: List<AapSetting.AncMode.Value> by lazy {
@@ -95,31 +79,32 @@ class DefaultAapDeviceProfile(
)
override fun encodeCommand(command: AapCommand): ByteArray = when (command) {
is AapCommand.SetAncMode -> buildSettingsMessage(SETTING_ANC_MODE, encodeAncMode(command.mode))
is AapCommand.SetConversationalAwareness -> buildSettingsMessage(SETTING_CONVERSATIONAL_AWARENESS, encodeAppleBool(command.enabled))
is AapCommand.SetPressSpeed -> buildSettingsMessage(SETTING_PRESS_SPEED, command.value.wireValue)
is AapCommand.SetPressHoldDuration -> buildSettingsMessage(SETTING_PRESS_HOLD_DURATION, command.value.wireValue)
is AapCommand.SetNcWithOneAirPod -> buildSettingsMessage(SETTING_NC_ONE_AIRPOD, encodeAppleBool(command.enabled))
is AapCommand.SetToneVolume -> buildSettingsMessage(SETTING_TONE_VOLUME, command.level.coerceIn(0x0F, 0x64))
is AapCommand.SetVolumeSwipeLength -> buildSettingsMessage(SETTING_VOLUME_SWIPE_LENGTH, command.value.wireValue)
is AapCommand.SetVolumeSwipe -> buildSettingsMessage(SETTING_VOLUME_SWIPE, encodeAppleBool(command.enabled))
is AapCommand.SetPersonalizedVolume -> buildSettingsMessage(SETTING_PERSONALIZED_VOLUME, encodeAppleBool(command.enabled))
is AapCommand.SetAncMode -> buildSettingsMessage(AapControlId.LISTEN_MODE.value, encodeAncMode(command.mode))
is AapCommand.SetConversationalAwareness -> buildSettingsMessage(AapControlId.CONVERSATION_DETECT.value, encodeAppleBool(command.enabled))
is AapCommand.SetPressSpeed -> buildSettingsMessage(AapControlId.DOUBLE_CLICK_INTERVAL.value, command.value.wireValue)
is AapCommand.SetPressHoldDuration -> buildSettingsMessage(AapControlId.CLICK_AND_HOLD_INTERVAL.value, command.value.wireValue)
is AapCommand.SetNcWithOneAirPod -> buildSettingsMessage(AapControlId.ONE_BUD_ANC_MODE.value, encodeAppleBool(command.enabled))
is AapCommand.SetToneVolume -> buildSettingsMessage(AapControlId.CHIME_VOLUME.value, command.level.coerceIn(0x0F, 0x64))
is AapCommand.SetVolumeSwipeLength -> buildSettingsMessage(AapControlId.VOLUME_SWIPE_INTERVAL.value, command.value.wireValue)
is AapCommand.SetVolumeSwipe -> buildSettingsMessage(AapControlId.VOLUME_SWIPE_MODE.value, encodeAppleBool(command.enabled))
is AapCommand.SetPersonalizedVolume -> buildSettingsMessage(AapControlId.ADAPTIVE_VOLUME.value, encodeAppleBool(command.enabled))
// Wire semantics are inverted: wire 0 = max noise reduction, wire 100 = min (transparency-like).
// UI value 0..100 follows user intuition (100 = max NC), so flip on write/read.
is AapCommand.SetAdaptiveAudioNoise -> buildSettingsMessage(SETTING_ADAPTIVE_AUDIO_NOISE, 100 - command.level.coerceIn(0, 100))
is AapCommand.SetAdaptiveAudioNoise -> buildSettingsMessage(AapControlId.AUTO_ANC_STRENGTH.value, 100 - command.level.coerceIn(0, 100))
is AapCommand.SetEndCallMuteMic -> buildEndCallMuteMicMessage(command.muteMic, command.endCall)
is AapCommand.SetMicrophoneMode -> buildSettingsMessage(SETTING_MICROPHONE_MODE, command.mode.wireValue)
is AapCommand.SetEarDetectionEnabled -> buildSettingsMessage(SETTING_EAR_DETECTION_ENABLED, encodeAppleBool(command.enabled))
is AapCommand.SetListeningModeCycle -> buildSettingsMessage(SETTING_LISTENING_MODE_CYCLE, command.modeMask and 0x0F)
is AapCommand.SetAllowOffOption -> buildSettingsMessage(SETTING_ALLOW_OFF_OPTION, encodeAppleBool(command.enabled))
is AapCommand.SetStemConfig -> buildSettingsMessage(SETTING_STEM_CONFIG, command.claimedPressMask and 0x0F)
is AapCommand.SetSleepDetection -> buildSettingsMessage(SETTING_SLEEP_DETECTION, encodeAppleBool(command.enabled))
is AapCommand.SetMicrophoneMode -> buildSettingsMessage(AapControlId.MIC_MODE.value, command.mode.wireValue)
is AapCommand.SetEarDetectionEnabled -> buildSettingsMessage(AapControlId.IN_EAR_DETECTION.value, encodeAppleBool(command.enabled))
is AapCommand.SetListeningModeCycle -> buildSettingsMessage(AapControlId.LISTENING_MODE_CONFIGS.value, command.modeMask and 0x0F)
is AapCommand.SetAllowOffOption -> buildSettingsMessage(AapControlId.ALLOW_OFF_OPTION.value, encodeAppleBool(command.enabled))
is AapCommand.SetStemConfig -> buildSettingsMessage(AapControlId.RAW_GESTURES_CONFIG.value, command.claimedPressMask and 0x0F)
is AapCommand.SetSleepDetection -> buildSettingsMessage(AapControlId.SLEEP_DETECTION.value, encodeAppleBool(command.enabled))
is AapCommand.SetDynamicEndOfCharge -> buildSettingsMessage(AapControlId.DYNAMIC_END_OF_CHARGE.value, encodeAppleBool(command.enabled))
is AapCommand.SetDeviceName -> buildRenameMessage(command.name)
}
override fun decodeSetting(message: AapMessage): Pair<KClass<out AapSetting>, AapSetting>? {
// Primary pod identity (push-only, fires on mic/primary swap)
if (message.commandType == CMD_PRIMARY_POD) {
if (message.commandType == AapMessageType.BUD_ROLE.value) {
if (message.payload.size < 4) return null
val podId = message.payload[0].toInt() and 0xFF
// Validate known fixed bytes: [podId] 00 [00|01] [00|01]
@@ -136,7 +121,7 @@ class DefaultAapDeviceProfile(
}
// Ear detection is a separate command type (push-only from device)
if (message.commandType == CMD_EAR_DETECTION) {
if (message.commandType == AapMessageType.EAR_DETECTION.value) {
if (message.payload.size < 2) return null
return AapSetting.EarDetection::class to AapSetting.EarDetection(
primaryPod = decodePodPlacement(message.payload[0].toInt() and 0xFF),
@@ -145,7 +130,7 @@ class DefaultAapDeviceProfile(
}
// Connected devices list (push-only from device)
if (message.commandType == CMD_CONNECTED_DEVICES) {
if (message.commandType == AapMessageType.CONNECTED_DEVICES.value) {
if (message.payload.size < 3) return null
val count = message.payload[2].toInt() and 0xFF
val devices = mutableListOf<AapSetting.ConnectedDevices.ConnectedDevice>()
@@ -161,7 +146,7 @@ class DefaultAapDeviceProfile(
}
// Audio source tracking (push-only from device)
if (message.commandType == CMD_AUDIO_SOURCE) {
if (message.commandType == AapMessageType.AUDIO_SOURCE.value) {
if (message.payload.size < 7) return null
val mac = (0 until 6).map { "%02X".format(message.payload[it]) }.joinToString(":")
val typeValue = message.payload[6].toInt() and 0xFF
@@ -173,8 +158,13 @@ class DefaultAapDeviceProfile(
return AapSetting.AudioSource::class to AapSetting.AudioSource(mac, type)
}
// EQ data (push-only from device)
if (message.commandType == CMD_EQ_DATA) {
// 0x53 is "PME Config" per the Wireshark AAP dissector — Personal Medical
// Equipment (cf. PPE), i.e. the iOS 18.1+ hearing-aid profile on AirPods
// Pro 2. Decoded verbatim as 4 × 8 Float32 (per-ear × per-profile band
// gains); stock firmware reports all-zero until the user runs Apple's
// Hearing Test. 0x54 "Set Band Edges" is a neighbouring opcode with a
// different payload — not decoded here.
if (message.commandType == AapMessageType.PME_CONFIG.value) {
if (message.payload.size < 6 + 128) return null
val sets = mutableListOf<List<Float>>()
var offset = 6 // skip header
@@ -190,92 +180,105 @@ class DefaultAapDeviceProfile(
}
sets.add(bands)
}
return AapSetting.EqBands::class to AapSetting.EqBands(sets)
return AapSetting.PmeConfig::class to AapSetting.PmeConfig(sets)
}
// Conversation Awareness State is a separate command type (push-only)
if (message.commandType == CMD_CONVERSATION_AWARENESS_STATE) {
if (message.commandType == AapMessageType.CONVERSATIONAL_AWARENESS.value) {
if (message.payload.isEmpty()) return null
val value = message.payload[0].toInt() and 0xFF
val speaking = value == 0x01
return AapSetting.ConversationalAwarenessState::class to AapSetting.ConversationalAwarenessState(speaking)
}
if (message.commandType != CMD_SETTINGS) return null
if (message.commandType != AapMessageType.CONTROL.value) return null
if (message.payload.size < 2) return null
val settingId = message.payload[0].toInt() and 0xFF
val value = message.payload[1].toInt() and 0xFF
return when (settingId) {
SETTING_ANC_MODE -> {
AapControlId.LISTEN_MODE.value -> {
val mode = decodeAncMode(value) ?: return null
AapSetting.AncMode::class to AapSetting.AncMode(current = mode, supported = supportedAncModes)
}
SETTING_CONVERSATIONAL_AWARENESS -> {
AapControlId.CONVERSATION_DETECT.value -> {
val enabled = decodeAppleBool(value) ?: return null
AapSetting.ConversationalAwareness::class to AapSetting.ConversationalAwareness(enabled)
}
SETTING_PRESS_SPEED -> {
AapControlId.DOUBLE_CLICK_INTERVAL.value -> {
val speed = AapSetting.PressSpeed.Value.fromWire(value) ?: return null
AapSetting.PressSpeed::class to AapSetting.PressSpeed(speed)
}
SETTING_PRESS_HOLD_DURATION -> {
AapControlId.CLICK_AND_HOLD_INTERVAL.value -> {
val duration = AapSetting.PressHoldDuration.Value.fromWire(value) ?: return null
AapSetting.PressHoldDuration::class to AapSetting.PressHoldDuration(duration)
}
SETTING_NC_ONE_AIRPOD -> {
AapControlId.ONE_BUD_ANC_MODE.value -> {
val enabled = decodeAppleBool(value) ?: return null
AapSetting.NcWithOneAirPod::class to AapSetting.NcWithOneAirPod(enabled)
}
SETTING_TONE_VOLUME -> {
AapControlId.CHIME_VOLUME.value -> {
AapSetting.ToneVolume::class to AapSetting.ToneVolume(level = value)
}
SETTING_VOLUME_SWIPE_LENGTH -> {
AapControlId.VOLUME_SWIPE_INTERVAL.value -> {
val length = AapSetting.VolumeSwipeLength.Value.fromWire(value) ?: return null
AapSetting.VolumeSwipeLength::class to AapSetting.VolumeSwipeLength(length)
}
SETTING_END_CALL_MUTE_MIC -> {
AapControlId.CALL_MANAGEMENT_CONFIG.value -> {
decodeEndCallMuteMic(message.payload)
}
SETTING_VOLUME_SWIPE -> {
AapControlId.VOLUME_SWIPE_MODE.value -> {
val enabled = decodeAppleBool(value) ?: return null
AapSetting.VolumeSwipe::class to AapSetting.VolumeSwipe(enabled)
}
SETTING_PERSONALIZED_VOLUME -> {
AapControlId.ADAPTIVE_VOLUME.value -> {
// Wireshark calls 0x26 "Adaptive Volume"; CAPod ships the user-facing name
// "Personalized Volume" because that matches the iOS Settings.app label.
val enabled = decodeAppleBool(value) ?: return null
AapSetting.PersonalizedVolume::class to AapSetting.PersonalizedVolume(enabled)
}
SETTING_ADAPTIVE_AUDIO_NOISE -> {
AapControlId.AUTO_ANC_STRENGTH.value -> {
AapSetting.AdaptiveAudioNoise::class to AapSetting.AdaptiveAudioNoise(level = 100 - value.coerceIn(0, 100))
}
SETTING_MICROPHONE_MODE -> {
AapControlId.MIC_MODE.value -> {
val mode = AapSetting.MicrophoneMode.Mode.fromWire(value) ?: return null
AapSetting.MicrophoneMode::class to AapSetting.MicrophoneMode(mode)
}
SETTING_EAR_DETECTION_ENABLED -> {
AapControlId.IN_EAR_DETECTION.value -> {
val enabled = decodeAppleBool(value) ?: return null
AapSetting.EarDetectionEnabled::class to AapSetting.EarDetectionEnabled(enabled)
}
SETTING_LISTENING_MODE_CYCLE -> {
AapControlId.LISTENING_MODE_CONFIGS.value -> {
AapSetting.ListeningModeCycle::class to AapSetting.ListeningModeCycle(modeMask = value)
}
SETTING_ALLOW_OFF_OPTION -> {
AapControlId.ALLOW_OFF_OPTION.value -> {
val enabled = decodeAppleBool(value) ?: return null
AapSetting.AllowOffOption::class to AapSetting.AllowOffOption(enabled)
}
SETTING_STEM_CONFIG -> {
AapControlId.RAW_GESTURES_CONFIG.value -> {
AapSetting.StemConfig::class to AapSetting.StemConfig(claimedPressMask = value)
}
SETTING_SLEEP_DETECTION -> {
AapControlId.SLEEP_DETECTION.value -> {
val enabled = decodeAppleBool(value) ?: return null
AapSetting.SleepDetection::class to AapSetting.SleepDetection(enabled)
}
SETTING_IN_CASE_TONE -> {
AapControlId.DYNAMIC_END_OF_CHARGE.value -> {
// Apple's "Optimized Charge Limit" — Pro 3 pushes this on connect as value 0x01
// (enabled). decodeAppleBool rejects anything that isn't a confirmed bool so
// unknown encodings fall through to UnknownSetting logging rather than being
// coerced to false.
val enabled = decodeAppleBool(value) ?: return null
AapSetting.DynamicEndOfCharge::class to AapSetting.DynamicEndOfCharge(enabled)
}
AapControlId.IN_CASE_TONE.value -> {
// Decoded internally (never exposed in UI) to keep lastMessageAt fresh.
// Originally labeled "Charging Sounds" — the real case tones are controlled
// over ATT, not here; the actual effect of this setting is unknown.
val enabled = decodeAppleBool(value) ?: return null
AapSetting.InCaseTone::class to AapSetting.InCaseTone(enabled)
}
in UNCONFIRMED_SETTING_IDS -> {
in UNMAPPED_SETTING_IDS -> {
AapSetting.UnknownSetting::class to AapSetting.UnknownSetting(
settingId = settingId,
rawValue = value,
@@ -286,7 +289,7 @@ class DefaultAapDeviceProfile(
}
override fun decodeBattery(message: AapMessage): Map<AapPodState.BatteryType, AapPodState.Battery>? {
if (message.commandType != CMD_BATTERY) return null
if (message.commandType != AapMessageType.BATTERY_INFO.value) return null
val payload = message.payload
if (payload.isEmpty()) return null
@@ -328,7 +331,7 @@ class DefaultAapDeviceProfile(
)
override fun decodePrivateKeyResponse(message: AapMessage): KeyExchangeResult? {
if (message.commandType != CMD_PRIVATE_KEYS_RESPONSE) return null
if (message.commandType != AapMessageType.MAGIC_KEYS.value) return null
val payload = message.payload
if (payload.isEmpty()) return null
@@ -360,26 +363,31 @@ class DefaultAapDeviceProfile(
}
override fun decodeDeviceInfo(message: AapMessage): AapDeviceInfo? {
if (message.commandType != CMD_DEVICE_INFO) return null
if (message.commandType != AapMessageType.INFORMATION.value) return null
if (message.payload.size < 10) return null
// Device info payload contains null-terminated UTF-8 strings
// Format: [binary header] [NUL-delimited strings...]
val strings = parseNullTerminatedStrings(message.payload)
if (strings.size < 4) return null
val parsed = parseDeviceInfoPayload(message.payload) ?: return null
if (parsed.strings.size < 4) return null
val activeFirmware = parsed.strings.getOrElse(4) { "" }
val pendingFirmware = parsed.strings.getOrNull(5)?.takeIf { it.isNotBlank() && it != activeFirmware }
return AapDeviceInfo(
name = strings.getOrElse(0) { "" },
modelNumber = strings.getOrElse(1) { "" },
manufacturer = strings.getOrElse(2) { "" },
serialNumber = strings.getOrElse(3) { "" },
firmwareVersion = strings.getOrElse(4) { "" },
// Segments [5]=firmware dup, [6]=protocol version, [7]=updater app ID — skipped
// Segments [8] and [9] are individual earbud serials (observed on Pro 1, Pro 2, Pro 3)
leftEarbudSerial = strings.getOrNull(8)?.takeIf { it.isNotBlank() },
rightEarbudSerial = strings.getOrNull(9)?.takeIf { it.isNotBlank() },
// Segment [10] is the build number (e.g. "8454624")
buildNumber = strings.getOrNull(10)?.takeIf { it.isNotBlank() },
name = parsed.strings.getOrElse(0) { "" },
modelNumber = parsed.strings.getOrElse(1) { "" },
manufacturer = parsed.strings.getOrElse(2) { "" },
serialNumber = parsed.strings.getOrElse(3) { "" },
firmwareVersion = activeFirmware,
firmwareVersionPending = pendingFirmware,
hardwareVersion = parsed.strings.getOrNull(6)?.takeIf { it.isNotBlank() },
eaProtocolName = parsed.strings.getOrNull(7)?.takeIf { it.isNotBlank() },
leftEarbudSerial = parsed.strings.getOrNull(8)?.takeIf { it.isNotBlank() },
rightEarbudSerial = parsed.strings.getOrNull(9)?.takeIf { it.isNotBlank() },
marketingVersion = parsed.strings.getOrNull(10)?.takeIf { it.isNotBlank() },
leftEarbudUuid = parsed.leftEarbudUuid,
rightEarbudUuid = parsed.rightEarbudUuid,
leftEarbudFirstPaired = parsed.leftEarbudFirstPaired,
rightEarbudFirstPaired = parsed.rightEarbudFirstPaired,
)
}
@@ -445,7 +453,7 @@ class DefaultAapDeviceProfile(
return byteArrayOf(
0x04, 0x00, 0x04, 0x00,
0x09, 0x00,
SETTING_END_CALL_MUTE_MIC.toByte(), 0x20,
AapControlId.CALL_MANAGEMENT_CONFIG.value.toByte(), 0x20,
combined.toByte(),
0x00, 0x00,
)
@@ -480,13 +488,49 @@ class DefaultAapDeviceProfile(
}
override fun decodeStemPress(message: AapMessage): StemPressEvent? {
if (message.commandType != CMD_STEM_PRESS) return null
if (message.commandType != AapMessageType.STEM_PRESS.value) return null
if (message.payload.size < 2) return null
val pressType = StemPressEvent.PressType.fromWire(message.payload[0].toInt() and 0xFF) ?: return null
val bud = StemPressEvent.Bud.fromWire(message.payload[1].toInt() and 0xFF) ?: return null
return StemPressEvent(pressType, bud)
}
/**
* Case Info probing is allowlisted to models observed to respond. Pro 3 is the
* confirmed first entry; other models can be added once captures verify they
* reply to 0x22 with a 0x23 payload.
*/
override fun encodeCaseInfoRequest(): ByteArray? {
if (model !in CASE_INFO_ALLOWLIST) return null
// Fire-and-forget 6-byte template, matching the private key request shape
// (header + command type with no payload).
return byteArrayOf(
0x04, 0x00, 0x04, 0x00,
AapMessageType.CASE_INFO_REQUEST.value.toByte(), 0x00,
)
}
/**
* Best-effort decoder. Per the dissector, the payload contains a mix of
* binary VID/PID/color bytes plus a NUL-delimited name string. Without
* more captures we preserve the raw payload and leave individual fields
* null — future iteration can map specific offsets.
*/
override fun decodeCaseInfo(message: AapMessage): AapCaseInfo? {
if (message.commandType != AapMessageType.CASE_INFO.value) return null
return AapCaseInfo(rawPayload = message.payload.copyOf())
}
override fun decodeSleepEvent(message: AapMessage): AapSleepEvent? {
if (message.commandType != AapMessageType.SLEEP_DETECTION_UPDATE.value) return null
return AapSleepEvent(rawPayload = message.payload.copyOf())
}
override fun decodeDynamicEndOfChargeEvent(message: AapMessage): AapDynamicEndOfChargeEvent? {
if (message.commandType != AapMessageType.DYNAMIC_END_OF_CHARGE.value) return null
return AapDynamicEndOfChargeEvent(rawPayload = message.payload.copyOf())
}
private fun buildRenameMessage(name: String): ByteArray {
// Uses the opcode 0x1A format from the LibrePods AAP docs + Linux implementation.
// Verified end-to-end on AirPods Pro 2 USB-C (firmware 81.2675...): the device accepts
@@ -510,31 +554,89 @@ class DefaultAapDeviceProfile(
) + nameBytes
}
private fun parseNullTerminatedStrings(data: ByteArray): List<String> {
// Skip binary header until the first printable byte (device name always starts
// with a printable character). This skips the length/type prefix bytes.
var headerEnd = 0
while (headerEnd < data.size) {
val b = data[headerEnd].toInt() and 0xFF
if (b in 0x20..0x7E) break
headerEnd++
/**
* Heuristic binary-prefix skip: walk forward until we hit a printable byte.
* The real header schema is `[02 XX 00 04 00]` in every capture to date, but
* documenting that without an authoritative source would be guessing. The
* heuristic breaks only for device names that start with a non-printable
* byte \u2014 theoretically emoji names (UTF-8 first byte 0xF0-0xF4, outside the
* printable range) could misalign here. None observed in real captures so far.
*/
private fun skipBinaryHeader(data: ByteArray): Int {
var offset = 0
while (offset < data.size) {
val b = data[offset].toInt() and 0xFF
if (b in 0x20..0x7E) return offset
offset++
}
if (headerEnd >= data.size) return emptyList()
// Split on NUL bytes and decode each segment as UTF-8.
// This handles device names with non-ASCII characters (e.g. curly quotes
// in "Matthias\u2019s AirPods Pro") that the old ASCII-only scanner would break on.
val strings = mutableListOf<String>()
var i = headerEnd
while (i < data.size) {
// Skip NUL separators
while (i < data.size && data[i] == 0x00.toByte()) i++
if (i >= data.size) break
val segStart = i
while (i < data.size && data[i] != 0x00.toByte()) i++
strings.add(String(data, segStart, i - segStart, Charsets.UTF_8))
}
return strings
return data.size
}
private fun parseDeviceInfoPayload(data: ByteArray): DeviceInfoSegments? {
var offset = skipBinaryHeader(data)
if (offset >= data.size) return null
// Segments 0..10: NUL-delimited UTF-8 strings. UTF-8 means non-ASCII (e.g.
// curly quotes in "Matthias's AirPods Pro") decodes correctly.
val strings = mutableListOf<String>()
while (strings.size < 11 && offset < data.size) {
while (offset < data.size && data[offset] == 0x00.toByte()) offset++
if (offset >= data.size) break
val segStart = offset
while (offset < data.size && data[offset] != 0x00.toByte()) offset++
strings.add(String(data, segStart, offset - segStart, Charsets.UTF_8))
}
// Skip the NUL that terminated segment 10 (if present) before the UUID blob.
while (offset < data.size && data[offset] == 0x00.toByte()) offset++
// Segments 11 and 12: fixed 17-byte UUIDs (NOT NUL-terminated, may contain 0x00).
// Best-effort: if payload is shorter, both UUIDs are null.
val leftUuid: ByteArray? = if (offset + UUID_LEN <= data.size) {
val slice = data.copyOfRange(offset, offset + UUID_LEN)
offset += UUID_LEN
slice
} else null
val rightUuid: ByteArray? = if (offset + UUID_LEN <= data.size) {
val slice = data.copyOfRange(offset, offset + UUID_LEN)
offset += UUID_LEN
slice
} else null
// Segments 13 and 14: NUL-delimited ASCII-decimal epoch seconds (e.g. "1697480211").
val trailingStrings = mutableListOf<String>()
while (trailingStrings.size < 2 && offset < data.size) {
while (offset < data.size && data[offset] == 0x00.toByte()) offset++
if (offset >= data.size) break
val segStart = offset
while (offset < data.size && data[offset] != 0x00.toByte()) offset++
trailingStrings.add(String(data, segStart, offset - segStart, Charsets.UTF_8))
}
val leftPaired = trailingStrings.getOrNull(0).parseEpochSecondsOrNull()
val rightPaired = trailingStrings.getOrNull(1).parseEpochSecondsOrNull()
return DeviceInfoSegments(
strings = strings,
leftEarbudUuid = leftUuid,
rightEarbudUuid = rightUuid,
leftEarbudFirstPaired = leftPaired,
rightEarbudFirstPaired = rightPaired,
)
}
private fun String?.parseEpochSecondsOrNull(): java.time.Instant? {
if (this.isNullOrBlank()) return null
val seconds = this.toLongOrNull() ?: return null
return runCatching { java.time.Instant.ofEpochSecond(seconds) }.getOrNull()
}
private data class DeviceInfoSegments(
val strings: List<String>,
val leftEarbudUuid: ByteArray?,
val rightEarbudUuid: ByteArray?,
val leftEarbudFirstPaired: java.time.Instant?,
val rightEarbudFirstPaired: java.time.Instant?,
)
}
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.logSummary
import eu.darken.capod.common.bluetooth.redactedForLogs
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
@@ -119,7 +120,7 @@ class AppleFactory @Inject constructor(
}
}
factory.create(
val device = factory.create(
scanResult = scanResult,
payload = payload,
meta = ApplePods.AppleMeta(
@@ -127,6 +128,18 @@ class AppleFactory @Inject constructor(
profile = profile,
),
)
log(TAG, DEBUG) {
val rawHex = scanResult.manufacturerSpecificData.entries.joinToString("; ") { (id, bytes) ->
"$id:${bytes.joinToString(" ") { "%02X".format(it.toInt() and 0xFF) }}"
}
val publicHex = payload.public.data.joinToString(" ") { "%02X".format(it.toInt()) }
val privateHex = payload.private?.data?.joinToString(" ") { "%02X".format(it.toInt()) } ?: "-"
"Apple decoded: model=${device.model}, addr=${scanResult.address.redactedForLogs()}, " +
"irkMatch=$isIrkMatch, raw=[$rawHex], public=[$publicHex], private=[$privateHex]"
}
device
}
companion object {
@@ -3,7 +3,6 @@ package eu.darken.capod.pods.core.apple.ble
import dagger.Reusable
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.logSummary
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
@@ -26,7 +25,7 @@ class PodFactory @Inject constructor(
device = unknownFactory.create(scanResult)
}
log(TAG, DEBUG) { "Pod created: ${device.logSummary()}" }
log(TAG, VERBOSE) { "Pod created: ${device.logSummary()}" }
return Result(scanResult = scanResult, device = device)
}
@@ -37,14 +37,14 @@ data class ProximityMessage(
log(
TAG,
Logging.Priority.ERROR
) { "Failed to decrypt $message with ${key.toByteString()}\n${e.asLog()}" }
) { "Failed to decrypt $message\n${e.asLog()}" }
null
}
log(
TAG,
Logging.Priority.VERBOSE
) { "Decrypted $message with ${key.toByteString()} to ${decryptedData?.toByteString()}" }
) { "Decrypted $message to ${decryptedData?.toByteString()}" }
if (decryptedData == null || decryptedData.size != 16) return null
@@ -55,4 +55,19 @@ data class AppleDeviceProfile(
showPopUpOnCaseOpen = showPopUpOnCaseOpen,
showPopUpOnConnection = showPopUpOnConnection,
)
override fun toString(): String = "AppleDeviceProfile(" +
"id=$id, label=$label, priority=$priority, model=$model, " +
"minimumSignalQuality=$minimumSignalQuality, " +
"identityKey=${if (identityKey == null) "null" else "<redacted>"}, " +
"encryptionKey=${if (encryptionKey == null) "null" else "<redacted>"}, " +
"address=$address, autoPause=$autoPause, autoPlay=$autoPlay, " +
"onePodMode=$onePodMode, autoConnect=$autoConnect, " +
"autoConnectCondition=$autoConnectCondition, " +
"showPopUpOnCaseOpen=$showPopUpOnCaseOpen, " +
"showPopUpOnConnection=$showPopUpOnConnection, " +
"learnedAllowOffEnabled=$learnedAllowOffEnabled, " +
"lastRequestedListeningModeCycleMask=$lastRequestedListeningModeCycleMask, " +
"stemActions=$stemActions" +
")"
}
@@ -0,0 +1,68 @@
package eu.darken.capod.reaction.core.sleep
import eu.darken.capod.common.MediaControl
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.monitor.core.DeviceMonitor
import eu.darken.capod.monitor.core.primaryDevice
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SleepReaction @Inject constructor(
private val aapManager: AapConnectionManager,
private val deviceMonitor: DeviceMonitor,
private val mediaControl: MediaControl,
private val notifications: SleepReactionNotifications,
private val timeSource: TimeSource,
) {
private val cooldowns = ConcurrentHashMap<BluetoothAddress, Long>()
fun monitor(): Flow<Unit> = aapManager.sleepEvents
.onEach { address -> handle(address) }
.map { }
.setupCommonEventHandlers(TAG) { "sleepReaction" }
private suspend fun handle(address: BluetoothAddress) {
val now = timeSource.elapsedRealtime()
val last = cooldowns[address]
if (last != null && now - last < COOLDOWN_MS) {
log(TAG) { "Sleep event from $address suppressed by cooldown" }
return
}
val primary = deviceMonitor.primaryDevice().first()
if (primary?.address != address) {
log(TAG) { "Sleep event from $address ignored — not primary device (primary=${primary?.address})" }
return
}
// Use sendPause's return value as the atomic check+act: true means we really paused
// something, false means nothing was playing. Gating the cooldown and notification on
// this closes the race where audio could stop between an upfront isPlaying check and
// the key dispatch, and avoids burning the 5-minute window on no-ops.
val paused = mediaControl.sendPause()
if (!paused) {
log(TAG) { "Sleep event from $address ignored — nothing was playing" }
return
}
cooldowns[address] = now
val label = primary.label ?: primary.model.label
log(TAG, INFO) { "Sleep detected on $address ($label) — paused media, notifying" }
notifications.show(label)
}
companion object {
private val TAG = logTag("Reaction", "Sleep")
private const val COOLDOWN_MS = 5L * 60L * 1000L
}
}
@@ -0,0 +1,66 @@
package eu.darken.capod.reaction.core.sleep
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.notifications.PendingIntentCompat
import eu.darken.capod.main.ui.MainActivity
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SleepReactionNotifications @Inject constructor(
@ApplicationContext private val context: Context,
private val notificationManager: NotificationManager,
) {
init {
notificationManager.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
context.getString(R.string.reaction_sleep_channel_label),
NotificationManager.IMPORTANCE_LOW,
)
)
}
fun show(deviceLabel: String) {
if (!notificationManager.areNotificationsEnabled()) {
log(TAG, WARN) { "Notifications disabled — sleep pause will be silent" }
return
}
val openPi = PendingIntent.getActivity(
context,
PENDING_INTENT_REQUEST_CODE,
Intent(context, MainActivity::class.java),
PendingIntentCompat.FLAG_IMMUTABLE,
)
val text = context.getString(R.string.reaction_sleep_notification_text, deviceLabel)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.device_earbuds_generic_both)
.setContentTitle(context.getString(R.string.reaction_sleep_notification_title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setContentIntent(openPi)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
companion object {
private val TAG = logTag("Reaction", "Sleep", "Notifications")
private val CHANNEL_ID = "${BuildConfigWrap.APPLICATION_ID}.notification.channel.reaction.sleep"
private const val NOTIFICATION_ID = 3
private const val PENDING_INTENT_REQUEST_CODE = 0
}
}
+18 -2
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Agtergrond-deursigtigheid</string>
<string name="widget_config_show_device_label">Wys toestelnaam</string>
<string name="widget_config_reset_label">Stel terug na standaarde</string>
<string name="widget_config_preview_resize_hint">U kan die widget vergroot of verklein nadat u dit op u tuisskerm geplaas het.</string>
<string name="widget_config_custom_label">Aangepas</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Donker</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Dit is \'n onbekende toestel, maar dit gebruik \'n soortgelyke boodskapformaat. Kom ons voeg ondersteuning daarvoor by, kontak my :)</string>
<string name="pods_none_label_short">Geen toestel</string>
<string name="pods_charging_label">Laai</string>
<string name="pods_charging_optimized_label">Geoptimaliseer</string>
<string name="pods_inear_label">In oor</string>
<string name="pods_microphone_label">Mikrofoon</string>
<string name="anc_mode_off">Af</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth-toestelaanduiding</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">Vervaardiger</string>
<string name="device_settings_info_hardware_label">Hardeware</string>
<string name="device_settings_info_serial_label">Serienommer</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Hangende Firmware</string>
<string name="device_settings_info_build_label">Bou</string>
<string name="device_settings_info_left_serial_label">Linker Pod Serienommer</string>
<string name="device_settings_info_right_serial_label">Regter Pod Serienommer</string>
<string name="device_settings_info_left_bonded_label">Links Gekoppel</string>
<string name="device_settings_info_right_bonded_label">Regs Gekoppel</string>
<string name="device_settings_info_details_label">Toestelbesonderhede</string>
<string name="device_settings_info_details_action">Wys toestelbesonderhede</string>
<string name="device_settings_info_status_label">Status</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Hierdie toestel is naby maar nie aan hierdie foon gekoppel nie. Koppel om toegang tot instellings en beheer te verkry.</string>
<string name="device_settings_not_connected_connect_action">Koppel</string>
<string name="device_settings_not_connected_open_settings_action">Maak Bluetooth-instellings oop</string>
<string name="device_settings_not_nearby_label">Toestel nie naby nie</string>
<string name="device_settings_not_nearby_description">Instellings is slegs beskikbaar wanneer hierdie toestel naby is en aan jou foon gekoppel is.</string>
<string name="device_settings_aap_unavailable_label">Gevorderde instellings nie beskikbaar nie</string>
<string name="device_settings_aap_unavailable_description">Gevorderde AirPods-instellings vereis \'n Bluetooth-kenmerk wat nie op hierdie foon beskikbaar is nie. Dit kan deur \'n toekomstige Android-opdatering van jou toestelvervaardiger opgelos word.</string>
<string name="device_settings_aap_unavailable_action">Bekyk verenigbaarheidsnaspeurder</string>
<string name="device_settings_category_sound_label">Klank</string>
<string name="device_settings_category_controls_label">Beheer</string>
<string name="device_settings_nc_one_airpod_label">ANC met een pod</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Enkel druk om mikrofoon te demp</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Dubbel druk om oproep te beëindig</string>
<string name="device_settings_open_cd">Maak toestel-instellings oop</string>
<string name="overview_card_missing_paired_device_cd">Profiel het geen gekoppelde Bluetooth-toestel nie — tik om profiel te wysig</string>
<string name="overview_card_missing_paired_device">Hierdie profiel het geen gekoppelde Bluetooth-toestel nie.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Algemeen</string>
<string name="device_settings_microphone_mode_label">Mikrofoon</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Laat Af toe as \'n kiesbare geraasbeheermodus. Wanneer gedeaktiveer, slaan stingels Af oor tydens siklus.</string>
<string name="device_settings_sleep_detection_label">Slaapopsporing</string>
<string name="device_settings_sleep_detection_description">Pauses oudio outomaties wanneer jy aan die slaap raak</string>
<string name="reaction_sleep_channel_label">Slaapopsporing</string>
<string name="reaction_sleep_notification_title">Gepauseer deur Slaapopsporing</string>
<string name="reaction_sleep_notification_text">%1$s het aangemeld dat jy aan die slaap geraak het, so jou musiek is gepauseer. Jy kan dit in die toestelinstellings deaktiveer.</string>
<string name="device_settings_rename_label">Hernoem</string>
<string name="device_settings_rename_hint">Toestelnaam</string>
<string name="device_settings_rename_confirm">Hernoem</string>
@@ -492,10 +504,14 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth-instellings</string>
<string name="device_settings_send_failed">Kon nie instelling toepas nie: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Af-modus is nie op hierdie toestel geaktiveer nie. Aktiveer \"Laat Af-modus toe\" onder Geraasbeheersing.</string>
<string name="device_settings_category_battery_label">Batterij</string>
<string name="device_settings_charge_cap_label">Geoptimaliseerde Laailimiet</string>
<string name="device_settings_charge_cap_description">Leer jou roetine en laat laai rondom 80%% pouseer om batteryleeftyd te verleng, en vul die dopjes aan voordat jy hulle waarskynlik sal gebruik.</string>
<string name="device_settings_charge_cap_rejected_message">Geoptimaliseerde Laailimiet kon nie verander word nie. Dit is \'n eksperimentele funksie — meld asseblief as dit aanhou misluk.</string>
<string name="device_settings_category_connections_label">Gekoppelde toestelle</string>
<string name="device_settings_connected_devices_description">Ander toestelle wat tans aan hierdie AirPods gekoppel is</string>
<string name="device_settings_connected_device_label">Toestel %d</string>
<string name="device_settings_connected_device_call">In \n oproep</string>
<string name="device_settings_connected_device_call">In n oproep</string>
<string name="device_settings_connected_device_media">Speel media</string>
<string name="device_settings_noise_control_label">Geraasbeheer</string>
<string name="device_settings_eq_label">Egaliseerder</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">የዳራ ግልጽነት</string>
<string name="widget_config_show_device_label">የመሳሪያ ስም አሳይ</string>
<string name="widget_config_reset_label">ወደ ነባሪ መመለስ</string>
<string name="widget_config_preview_resize_hint">ዊጄቱን በዋናው ስክሪን ላይ ካስቀመጡት በኋላ መጠኑን መቀየር ይችላሉ።</string>
<string name="widget_config_custom_label">ብጁ</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">ጨለማ</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">ይህ ያልታወቀ መሣሪያ ነው ግን ተመሳሳይ የመልዕክት ቅርጸት ይጠቀማል። ድጋፍ እንጨምርለት፣ ያግኙኝ :)</string>
<string name="pods_none_label_short">ምንም መሣሪያ የለም</string>
<string name="pods_charging_label">በመሙላት ላይ</string>
<string name="pods_charging_optimized_label">ተመቻችቷል</string>
<string name="pods_inear_label">በጆሮ ውስጥ</string>
<string name="pods_microphone_label">ማይክሮፎን</string>
<string name="anc_mode_off">ጠፍቷል</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">የብሉቱዝ መሳሪያ ስያሜ</string>
<string name="device_settings_info_model_label">ሞዴል</string>
<string name="device_settings_info_manufacturer_label">አምራች</string>
<string name="device_settings_info_hardware_label">ሃርድዌር</string>
<string name="device_settings_info_serial_label">ተከታታይ ቁጥር</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">ወደ ላይ ለሚጫን ፈርምዌር</string>
<string name="device_settings_info_build_label">ግንባታ</string>
<string name="device_settings_info_left_serial_label">የግራ ፖድ ተከታታይ ቅጥር</string>
<string name="device_settings_info_right_serial_label">የቀኝ ፖድ ተከታታይ ቅጥር</string>
<string name="device_settings_info_left_bonded_label">ግራ ተያይዟል</string>
<string name="device_settings_info_right_bonded_label">ቀኝ ተያይዟል</string>
<string name="device_settings_info_details_label">የመሳሪያ ዝርዝሮች</string>
<string name="device_settings_info_details_action">የመሳሪያ ዝርዝሮችን አሳይ</string>
<string name="device_settings_info_status_label">ሁኔታ</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">ይህ መሣሪያ አቅራቢያ አለ ነገር ግን ከዚህ ስልክ ጋር አልተገናኘም። ቅንብሮችን እና መቆጣጠሪያዎችን ለማግኘት ያገናኙ።</string>
<string name="device_settings_not_connected_connect_action">ያገናኙ</string>
<string name="device_settings_not_connected_open_settings_action">የብሉቱዝ ቅንብሮችን ይክፈቱ</string>
<string name="device_settings_not_nearby_label">መሣሪያው ቅርብ አይደለም</string>
<string name="device_settings_not_nearby_description">ቅንብሮቹ የሚገኙት ይህ መሣሪያ ቅርብ ሆኖ ከስልክዎ ጋር ሲገናኝ ብቻ ነው።</string>
<string name="device_settings_aap_unavailable_label">የላቁ ቅንብሮች አይገኙም</string>
<string name="device_settings_aap_unavailable_description">የላቀ AirPods ቅንብሮች በዚህ ስለክ ላይ የማይገኝ የብሉቱዝ ባህሪ ይፈልጋሉ። ይህ ከወደፊቱ ከመሳሪያ አምራችዎ የሚመጣ የAndroid ዝማኔ ሊፈታ ይችላል።</string>
<string name="device_settings_aap_unavailable_action">የተኳሃኝነት ተከታይ ይመልከቱ</string>
<string name="device_settings_category_sound_label">ድምፅ</string>
<string name="device_settings_category_controls_label">መቀጣጣሪያዎች</string>
<string name="device_settings_nc_one_airpod_label">ANC በአንድ ፖድ</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">ማይክሮፎኑን ለማጥፈት አንድ ጊዜ ጀን</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">ጥሪን ለመጨረስ ሁለት ጊዜ ጀን</string>
<string name="device_settings_open_cd">የመሣሪያ ቅንብሮችን ክፈት</string>
<string name="overview_card_missing_paired_device_cd">ፕሮፋይሉ ምንም ተጣምሮ የ Bluetooth መሳሪያ የለም — ፕሮፋይሉን ለማስተካከል መታ ያድርጉ</string>
<string name="overview_card_missing_paired_device">ይህ መገለጫ የጋራ ብሉቱዝ መሳሪያ የለውም።</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">አጠቃላይ</string>
<string name="device_settings_microphone_mode_label">ማይክሮፎን</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">ጠፍን እንደ ሊመረጥ የሚችል የጫጫታ ቁጥጥር ሁኔታ ፍቀድ። ሲሰናከል፣ ቀንዶቹ ዑደት ሲያደርጉ ጠፍን ይዝለሉ።</string>
<string name="device_settings_sleep_detection_label">እንቅልፍ ማወቃያ</string>
<string name="device_settings_sleep_detection_description">ሲተኝ ድምጹን ራስ-ሰር አቁም</string>
<string name="reaction_sleep_channel_label">የእንቅልፍ ማወቂያ</string>
<string name="reaction_sleep_notification_title">በእንቅልፍ ማወቂያ ምክንያት ታግዷል</string>
<string name="reaction_sleep_notification_text">%1$s እርስዎ ተኝተዋል ብሎ ዘገበ፣ ስለዚህ ሙዚቃዎ ተቋርጧል። ይህን በመሣሪያ ቅንብሮች ውስጥ ማሰናከል ይችላሉ።</string>
<string name="device_settings_rename_label">ስም ቀይር</string>
<string name="device_settings_rename_hint">የመሣሪያ ስም</string>
<string name="device_settings_rename_confirm">ስም ቀይር</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">የ Bluetooth ቅንብሮች</string>
<string name="device_settings_send_failed">ቅንብሩን መተግበር አልተቻለም: %1$s</string>
<string name="device_settings_anc_off_rejected_message">ጠፍ ሁኔታ በዚህ መሳሪያ ላይ አልነቃም። \"ጠፍ ሁኔታን ፍቀድ\" በጫጫታ ቁጥጥር ስር አንቃ።</string>
<string name="device_settings_category_battery_label">ባትሪ</string>
<string name="device_settings_charge_cap_label">ተመቻችቶ የሚቆጠር የኃይል ወሰን</string>
<string name="device_settings_charge_cap_description">የእርስዎን ልምድ ተምሮ የባትሪ ሕይወት ለማሳደግ ወደ 80%% ሲደርስ ኤሌክትሪኩን ያቆማል፣ ሊጠቀሟቸው ከመሆናቸው በፊት ድብዳቤ ኮዳቹን ሙሉ ይሞላቸዋል።</string>
<string name="device_settings_charge_cap_rejected_message">ተመቻችቶ የሚቆጠር የኃይል ወሰን ሊቀየር አልቻለም። ይህ የሙከራ ባህሪ ነው — አሁንም ቢሆን ካልሰራ እባክዎ ሪፖርት ያድርጉ።</string>
<string name="device_settings_category_connections_label">የተገናኙ መሣሪያዎች</string>
<string name="device_settings_connected_devices_description">አሁን ለእነዚህ AirPods የተገናኙ ሌሎች መሣሪያዎች</string>
<string name="device_settings_connected_device_label">መሣሪያ %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">شفافية الخلفية</string>
<string name="widget_config_show_device_label">إظهار اسم الجهاز</string>
<string name="widget_config_reset_label">إعادة تعيين إلى الإعدادات الافتراضية</string>
<string name="widget_config_preview_resize_hint">يمكنك تغيير حجم الأداة بعد وضعها على شاشتك الرئيسية.</string>
<string name="widget_config_custom_label">مخصص</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">مظلم</string>
@@ -258,6 +259,7 @@
<string name="pods_unknown_contact_dev">هذا جهاز غير معروف، لكنه يستخدم تنسيق رسائل مشابهًا. دعونا نضيف دعمًا له، تواصل معي :)</string>
<string name="pods_none_label_short">لا يوجد جهاز</string>
<string name="pods_charging_label">جارٍ الشحن</string>
<string name="pods_charging_optimized_label">محسَّن</string>
<string name="pods_inear_label">في الأذن</string>
<string name="pods_microphone_label">الميكروفون</string>
<string name="anc_mode_off">إيقاف</string>
@@ -427,11 +429,15 @@
<string name="device_settings_info_bt_name_label">تسمية جهاز البلوتوث</string>
<string name="device_settings_info_model_label">الطراز</string>
<string name="device_settings_info_manufacturer_label">الشركة المصنّعة</string>
<string name="device_settings_info_hardware_label">الجهاز</string>
<string name="device_settings_info_serial_label">الرقم التسلسلي</string>
<string name="device_settings_info_firmware_label">البرنامج الثابت</string>
<string name="device_settings_info_firmware_pending_label">البرنامج الثابت المعلق</string>
<string name="device_settings_info_build_label">الإصدار</string>
<string name="device_settings_info_left_serial_label">الرقم التسلسلي للسماعة اليسرى</string>
<string name="device_settings_info_right_serial_label">الرقم التسلسلي للسماعة اليمنى</string>
<string name="device_settings_info_left_bonded_label">اليسار مقترن</string>
<string name="device_settings_info_right_bonded_label">اليمين مقترن</string>
<string name="device_settings_info_details_label">تفاصيل الجهاز</string>
<string name="device_settings_info_details_action">عرض تفاصيل الجهاز</string>
<string name="device_settings_info_status_label">الحالة</string>
@@ -441,8 +447,11 @@
<string name="device_settings_not_connected_description">هذا الجهاز قريب لكنه غير متصل بهذا الهاتف. اتصل للوصول إلى الإعدادات والعناصر.</string>
<string name="device_settings_not_connected_connect_action">اتصال</string>
<string name="device_settings_not_connected_open_settings_action">فتح إعدادات البلوتوث</string>
<string name="device_settings_not_nearby_label">الجهاز غير قريب</string>
<string name="device_settings_not_nearby_description">الإعدادات متاحة فقط عندما يكون هذا الجهاز قريبًا ومتصلًا بهاتفك.</string>
<string name="device_settings_aap_unavailable_label">الإعدادات المتقدمة غير متاحة</string>
<string name="device_settings_aap_unavailable_description">تتطلب إعدادات AirPods المتقدمة ميزة بلوتوث غير متاحة على هذا الهاتف. قد يتم حل هذا بتحديث Android مستقبلي من الشركة المصنّعة لجهازك.</string>
<string name="device_settings_aap_unavailable_action">عرض متتبع التوافق</string>
<string name="device_settings_category_sound_label">الصوت</string>
<string name="device_settings_category_controls_label">عناصر التحكم</string>
<string name="device_settings_nc_one_airpod_label">إلغاء ضوضاء نشط مع سماعة واحدة</string>
@@ -480,7 +489,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">ضغطة واحدة لكتم صوت الميكروفون</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">ضغطة مزدوجة لإنهاء المكالمة</string>
<string name="device_settings_open_cd">فتح إعدادات الجهاز</string>
<string name="overview_card_missing_paired_device_cd">الملف الشخصي لا يحتوي على جهاز بلوتوث مقترن — انقر للتعديل</string>
<string name="overview_card_missing_paired_device">هذا الملف الشخصي لا يحتوي على جهاز بلوتوث مقترن.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">عام</string>
<string name="device_settings_microphone_mode_label">ميكروفون</string>
@@ -503,6 +512,9 @@
<string name="device_settings_allow_off_description">السماح بوضع إيقاف كوضع تحكم قابل للتحديد في الضوضاء. عند التعطيل، تتجاوز الذراعان وضع إيقاف أثناء الدوران.</string>
<string name="device_settings_sleep_detection_label">اكتشاف النوم</string>
<string name="device_settings_sleep_detection_description">إيقاف الصوت تلقائيًا عند النوم</string>
<string name="reaction_sleep_channel_label">اكتشاف النوم</string>
<string name="reaction_sleep_notification_title">تم الإيقاف المؤقت بواسطة اكتشاف النوم</string>
<string name="reaction_sleep_notification_text">%1$s أفاد بأنك نمت، لذا تم إيقاف موسيقاك مؤقتًا. يمكنك تعطيل هذا في إعدادات الجهاز.</string>
<string name="device_settings_rename_label">إعادة تسمية</string>
<string name="device_settings_rename_hint">اسم الجهاز</string>
<string name="device_settings_rename_confirm">إعادة تسمية</string>
@@ -512,6 +524,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">إعدادات البلوتوث</string>
<string name="device_settings_send_failed">تعذّر تطبيق الإعداد: %1$s</string>
<string name="device_settings_anc_off_rejected_message">وضع إيقاف غير مفعّل على هذا الجهاز. فعّل \"السماح بوضع إيقاف\" ضمن التحكم في الضوضاء.</string>
<string name="device_settings_category_battery_label">البطارية</string>
<string name="device_settings_charge_cap_label">حد الشحن المحسَّن</string>
<string name="device_settings_charge_cap_description">يتعلم روتينك ويوقف الشحن مؤقتًا عند حوالي 80%% لإطالة عمر البطارية، مع شحن السماعات بالكامل قبل استخدامك المتوقع لها.</string>
<string name="device_settings_charge_cap_rejected_message">تعذّر تغيير حد الشحن المحسَّن. هذه ميزة تجريبية — يرجى الإبلاغ إذا استمر الفشل.</string>
<string name="device_settings_category_connections_label">الأجهزة المتصلة</string>
<string name="device_settings_connected_devices_description">الأجهزة الأخرى المتصلة حاليًا بهذه AirPods</string>
<string name="device_settings_connected_device_label">الجهاز %d</string>
+18 -2
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Fon şəffaflığı</string>
<string name="widget_config_show_device_label">Cihaz adını göstər</string>
<string name="widget_config_reset_label">Varsayılan ayarlara sıfırla</string>
<string name="widget_config_preview_resize_hint">Vidceti ana ekranınıza yerləşdirdikdən sonra ölçüsünü dəyişə bilərsiniz.</string>
<string name="widget_config_custom_label">Fərdi</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Tünd</string>
@@ -233,7 +234,7 @@
<string name="pods_dual_left_label">Sol qulaqlıq</string>
<string name="pods_dual_right_label">Sağ qulaqlıq</string>
<string name="pods_dual_left_short_label">S</string>
<string name="pods_dual_right_short_label">S</string>
<string name="pods_dual_right_short_label">SĚ</string>
<string name="pods_case_label">Qutu</string>
<string name="battery_unavailable_label">Batareya məlumatı yoxdur</string>
<string name="pods_case_status_open_label">Açıq</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Bu naməlum bir cihazdır, lakin oxşar mesaj formatından istifadə edir. Onun üçün dəstək əlavə edək, mənimlə əlaqə saxlayın :)</string>
<string name="pods_none_label_short">Cihaz yoxdur</string>
<string name="pods_charging_label">Şarj olunur</string>
<string name="pods_charging_optimized_label">Optimallaşdırılmış</string>
<string name="pods_inear_label">Qulaqda</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Söndür</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth Cihaz Etiketə</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">İstehsalcı</string>
<string name="device_settings_info_hardware_label">Aparat</string>
<string name="device_settings_info_serial_label">Seriya Nömrəsi</string>
<string name="device_settings_info_firmware_label">Proqram təminı</string>
<string name="device_settings_info_firmware_pending_label">Gözləmədə olan proqram təminatı</string>
<string name="device_settings_info_build_label">Yazılım versiyası</string>
<string name="device_settings_info_left_serial_label">Sol Qulaqcıq Seriya Nömrəsi</string>
<string name="device_settings_info_right_serial_label">Sağ Qulaqcıq Seriya Nömrəsi</string>
<string name="device_settings_info_left_bonded_label">Sol cütləşdirilmiş</string>
<string name="device_settings_info_right_bonded_label">Sağ cütləşdirilmiş</string>
<string name="device_settings_info_details_label">Cihaz Ǝtraflı Məlumatları</string>
<string name="device_settings_info_details_action">Cihaz təfsilatlarını göstər</string>
<string name="device_settings_info_status_label">Vəziyyət</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Bu cihaz yaxında yerləşir, lakin bu telefona bağlı deyil. Parametrlərə və idarəetməyə daxil olmaq üçün qoşulun.</string>
<string name="device_settings_not_connected_connect_action">Qoşul</string>
<string name="device_settings_not_connected_open_settings_action">Bluetooth Parametrlərini Aç</string>
<string name="device_settings_not_nearby_label">Cihaz yaxında deyil</string>
<string name="device_settings_not_nearby_description">Parametrlər yalnız bu cihaz yaxında olduqda və telefonunuza qoşulduqda mövcuddur.</string>
<string name="device_settings_aap_unavailable_label">Ətraflı parametrlər əlçatmazdır</string>
<string name="device_settings_aap_unavailable_description">Qabaqcıl AirPods parametrləri bu telefondan əldati olunan Bluetooth funksiyasını tələb edir. Bu problem cihaz istehsalcınızdan gələcək Android yenilməsi ilə həll oluna bilər.</string>
<string name="device_settings_aap_unavailable_action">Uyğunluq izləyicisinə baxın</string>
<string name="device_settings_category_sound_label">Səs</string>
<string name="device_settings_category_controls_label">İdarəetmələr</string>
<string name="device_settings_nc_one_airpod_label">Tək qulaqcıqla ANC</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Mikrofonu susdurmağ üçün bir dəfə basın</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Zəngi bitirmək üçün iki dəfə basın</string>
<string name="device_settings_open_cd">Cihaz parametrlərini açın</string>
<string name="overview_card_missing_paired_device_cd">Profilin cütləşdirilmiş Bluetooth cihazı yoxdur — profili redaktə etmək üçün toxunun</string>
<string name="overview_card_missing_paired_device">Bu profilin cütləşdirilmiş Bluetooth cihazı yoxdur.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Ümumi</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Söndürmə rejiminin səs-küy nəzarətində seçilə bilməsinə icazə ver. Deaktiv edildçə, gövə dövr kəsindi Söndürməni atlayar.</string>
<string name="device_settings_sleep_detection_label">Yuxu Aşkarlama</string>
<string name="device_settings_sleep_detection_description">Yuxuya getdikdə audionu avtomatik dayandırın</string>
<string name="reaction_sleep_channel_label">Yuxu Aşkarlaması</string>
<string name="reaction_sleep_notification_title">Yuxu Aşkarlaması ilə dayandırıldı</string>
<string name="reaction_sleep_notification_text">%1$s sizin yuxuya getdiyinizi bildirdi, buna görə musiqiniz dayandırıldı. Bunu cihaz parametrlərindən söndürə bilərsiniz.</string>
<string name="device_settings_rename_label">Adı dəyişdirin</string>
<string name="device_settings_rename_hint">Cihaz adı</string>
<string name="device_settings_rename_confirm">Adı dəyişdirin</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth Parametrləri</string>
<string name="device_settings_send_failed">Parametr tətbiq edilə bilmədi: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Söndürmə rejimi bu cihazda aktiv deyil. Səs-Küy Nəzarəti altında \"Söndürmə rejiminə icazə ver\" seçimini aktiv edin.</string>
<string name="device_settings_category_battery_label">Batareya</string>
<string name="device_settings_charge_cap_label">Optimallaşdırılmış Şarj Həddi</string>
<string name="device_settings_charge_cap_description">Batareya ömrünü uzatmaq üçün rutininizi öyrənir və şarjı təxminən 80%%-də dayandırır, qulluqlardan istifadə etməzdən əvvəl onları şarj edir.</string>
<string name="device_settings_charge_cap_rejected_message">Optimallaşdırılmış Şarj Həddini dəyişdirmək mümkün olmadı. Bu eksperimental bir xüsusiyyətdir — davam edərsə xahiş edirik bildirin.</string>
<string name="device_settings_category_connections_label">Bağlı Cihazlar</string>
<string name="device_settings_connected_devices_description">Bu AirPodlara hal-hazırda bağlı olan digər cihazlar</string>
<string name="device_settings_connected_device_label">Cihaz %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Прозрачнасць фона</string>
<string name="widget_config_show_device_label">Паказаць назву прылады</string>
<string name="widget_config_reset_label">Скінуць да змаўчання</string>
<string name="widget_config_preview_resize_hint">Вы можаце змяніць памер віджэта пасля размяшчэння яго на галоўным экране.</string>
<string name="widget_config_custom_label">Адвольны</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Цёмны</string>
@@ -254,6 +255,7 @@
<string name="pods_unknown_contact_dev">Гэта невядомая прылада, але яна выкарыстоўвае падобны фармат паведамленняў. Давайце дадамо яе падтрымку, звяжыцеся са мной :)</string>
<string name="pods_none_label_short">Няма прылады</string>
<string name="pods_charging_label">Зарадка</string>
<string name="pods_charging_optimized_label">Аптымізавана</string>
<string name="pods_inear_label">У вуху</string>
<string name="pods_microphone_label">Мікрафон</string>
<string name="anc_mode_off">Выкл</string>
@@ -417,11 +419,15 @@
<string name="device_settings_info_bt_name_label">Назва Bluetooth-прылады</string>
<string name="device_settings_info_model_label">Мадэль</string>
<string name="device_settings_info_manufacturer_label">Вытворца</string>
<string name="device_settings_info_hardware_label">Абсталяванне</string>
<string name="device_settings_info_serial_label">Серыйны нумар</string>
<string name="device_settings_info_firmware_label">Прашыўка</string>
<string name="device_settings_info_firmware_pending_label">Прашыўка ў чарзе</string>
<string name="device_settings_info_build_label">Зборка</string>
<string name="device_settings_info_left_serial_label">Сэрыйны нумар левага навушніка</string>
<string name="device_settings_info_right_serial_label">Сэрыйны нумар правага навушніка</string>
<string name="device_settings_info_left_bonded_label">Левы спалучаны</string>
<string name="device_settings_info_right_bonded_label">Правы спалучаны</string>
<string name="device_settings_info_details_label">Дэталі прылады</string>
<string name="device_settings_info_details_action">Паказаць дэталі прылады</string>
<string name="device_settings_info_status_label">Стан</string>
@@ -431,8 +437,11 @@
<string name="device_settings_not_connected_description">Гэта прылада знаходзіцца побач, але не падключана да гэтага тэлефона. Падключыцеся, каб атрымаць доступ да налад і кіравання.</string>
<string name="device_settings_not_connected_connect_action">Падключыцца</string>
<string name="device_settings_not_connected_open_settings_action">Адкрыць налады Bluetooth</string>
<string name="device_settings_not_nearby_label">Прылада не паблізу</string>
<string name="device_settings_not_nearby_description">Налады даступныя толькі тады, калі гэтая прылада знаходзіцца паблізу і падключана да вашага тэлефона.</string>
<string name="device_settings_aap_unavailable_label">Пашыраныя налады недаступныя</string>
<string name="device_settings_aap_unavailable_description">Пашыраныя налады AirPods патрабуюць функцыі Bluetooth, якая недаступная на гэтым тэлефоне. Гэта можа быць вырашана будучым абнаўленнем Android ад вытворцы вашай прылады.</string>
<string name="device_settings_aap_unavailable_action">Праглядзець трэкер сумяшчальнасці</string>
<string name="device_settings_category_sound_label">Гук</string>
<string name="device_settings_category_controls_label">Кіраванне</string>
<string name="device_settings_nc_one_airpod_label">ANC з адным навушнікам</string>
@@ -470,7 +479,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Аднаразовы націск для адключэння мікрафона</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Двайны націск для завяршэння выкліку</string>
<string name="device_settings_open_cd">Адкрыць налады прылады</string>
<string name="overview_card_missing_paired_device_cd">У профілі няма спалучанай прылады Bluetooth — дакраніцеся, каб рэдагаваць профіль</string>
<string name="overview_card_missing_paired_device">З гэтым профілем не сапалучана прылада Bluetooth.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Агульныя</string>
<string name="device_settings_microphone_mode_label">Мікрафон</string>
@@ -493,6 +502,9 @@
<string name="device_settings_allow_off_description">Дазволіць рэжым «Выкл» як выбіральны рэжым кіравання шумам. Калі адключана, ножкі прапускаюць «Выкл» пры цыкле.</string>
<string name="device_settings_sleep_detection_label">Выяўленне сну</string>
<string name="device_settings_sleep_detection_description">Аўтаматычна ставіць аўдыё на паўзу, калі вы засынаеце</string>
<string name="reaction_sleep_channel_label">Выяўленне сну</string>
<string name="reaction_sleep_notification_title">Прыпынена функцыяй выяўлення сну</string>
<string name="reaction_sleep_notification_text">%1$s паведаміла, што вы заснулі, таму музыка была прыпынена. Вы можаце адключыць гэта ў наладах прылады.</string>
<string name="device_settings_rename_label">Перайменаваць</string>
<string name="device_settings_rename_hint">Назва прылады</string>
<string name="device_settings_rename_confirm">Перайменаваць</string>
@@ -502,6 +514,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Налады Bluetooth</string>
<string name="device_settings_send_failed">Не ўдалося прымяніць налады: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Рэжым «Выкл» не ўключаны на гэтай прыладзе. Уключыце «Дазволіць рэжым Выкл» ў раздзеле «Кіраванне шумам».</string>
<string name="device_settings_category_battery_label">Акумулятар</string>
<string name="device_settings_charge_cap_label">Аптымізаваная мяжа зарадкі</string>
<string name="device_settings_charge_cap_description">Вывучае ваш распарадак і прыпыняе зарадку каля 80%%, каб падоўжыць тэрмін службы акумулятара, дазараджаючы наўшпількі перад тым, як вы, верагодна, скарыстаецеся імі.</string>
<string name="device_settings_charge_cap_rejected_message">Аптымізаваную мяжу зарадкі не ўдалося змяніць. Гэта эксперыментальная функцыя — калі ласка, паведамляйце, калі яна ўвесь час не спрацоўвае.</string>
<string name="device_settings_category_connections_label">Падключаныя прылады</string>
<string name="device_settings_connected_devices_description">Іншыя прылады, якія цяпер падключаны да гэтых AirPods</string>
<string name="device_settings_connected_device_label">Прылада %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Прозрачност на фона</string>
<string name="widget_config_show_device_label">Показване на името на устройството</string>
<string name="widget_config_reset_label">Възстановяване на стандартните настройки</string>
<string name="widget_config_preview_resize_hint">Можете да преоразмерите джаджата, след като я поставите на началния си екран.</string>
<string name="widget_config_custom_label">Персонализирано</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Тъмно</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Това е неизвестно устройство, но използва подобен формат на съобщения. Нека добавим поддръжка за него, свържете се с мен :)</string>
<string name="pods_none_label_short">Няма устройство</string>
<string name="pods_charging_label">Зареждане</string>
<string name="pods_charging_optimized_label">Оптимизирано</string>
<string name="pods_inear_label">В ухото</string>
<string name="pods_microphone_label">Микрофон</string>
<string name="anc_mode_off">Изкл.</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth етикет на устройство</string>
<string name="device_settings_info_model_label">Модел</string>
<string name="device_settings_info_manufacturer_label">Производител</string>
<string name="device_settings_info_hardware_label">Хардуер</string>
<string name="device_settings_info_serial_label">Сериен номер</string>
<string name="device_settings_info_firmware_label">Фърмуер</string>
<string name="device_settings_info_firmware_pending_label">Изчакващ фърмуер</string>
<string name="device_settings_info_build_label">Версия</string>
<string name="device_settings_info_left_serial_label">Сериен номер на левия под</string>
<string name="device_settings_info_right_serial_label">Сериен номер на десния под</string>
<string name="device_settings_info_left_bonded_label">Ляво сдвоено</string>
<string name="device_settings_info_right_bonded_label">Дясно сдвоено</string>
<string name="device_settings_info_details_label">Подробности за устройството</string>
<string name="device_settings_info_details_action">Покажи подробности за устройството</string>
<string name="device_settings_info_status_label">Статус</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Устройството е наблизо, но не е свързано с този телефон. Свържете се, за да получите достъп до настройките и управлението.</string>
<string name="device_settings_not_connected_connect_action">Свържи се</string>
<string name="device_settings_not_connected_open_settings_action">Отвори настройките на Bluetooth</string>
<string name="device_settings_not_nearby_label">Устройството не е наблизо</string>
<string name="device_settings_not_nearby_description">Настройките са налични само когато устройството е наблизо и свързано с телефона ви.</string>
<string name="device_settings_aap_unavailable_label">Разширените настройки не са налични</string>
<string name="device_settings_aap_unavailable_description">Разширените настройки на AirPods изискват Bluetooth функция, която не е налична на този телефон. Това може да бъде разрешено от бъдеща актуализация на Android от производителя на вашето устройство.</string>
<string name="device_settings_aap_unavailable_action">Вижте проследяването на съвместимостта</string>
<string name="device_settings_category_sound_label">Звук</string>
<string name="device_settings_category_controls_label">Управление</string>
<string name="device_settings_nc_one_airpod_label">ANC с един под</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Едно натискане за заглушаване на микрофона</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Двойно натискане за прекратяване на разговора</string>
<string name="device_settings_open_cd">Отворете настройките на устройството</string>
<string name="overview_card_missing_paired_device_cd">Профилът няма свързано Bluetooth устройство — докоснете, за да редактирате профила</string>
<string name="overview_card_missing_paired_device">Този профил няма сдвоено Bluetooth устройство.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Общи</string>
<string name="device_settings_microphone_mode_label">Микрофон</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Разрешава Изключено като избираем режим за контрол на шума. Когато е деактивирано, стъблата пропускат Изключено при циклиране.</string>
<string name="device_settings_sleep_detection_label">Разпознаване на сън</string>
<string name="device_settings_sleep_detection_description">Автоматично поставя звука на пауза, когато заспите</string>
<string name="reaction_sleep_channel_label">Откриване на сън</string>
<string name="reaction_sleep_notification_title">Поставено на пауза от Откриване на сън</string>
<string name="reaction_sleep_notification_text">%1$s откри, че сте заспали, затова музиката ви беше поставена на пауза. Можете да деактивирате това в настройките на устройството.</string>
<string name="device_settings_rename_label">Преименуване</string>
<string name="device_settings_rename_hint">Наименование на устройството</string>
<string name="device_settings_rename_confirm">Преименуване</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth настройки</string>
<string name="device_settings_send_failed">Неуспешно прилагане на настройката: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Режимът Изключено не е активиран на това устройство. Активирайте \"Разрешаване на режим Изключено\" в Контрол на шума.</string>
<string name="device_settings_category_battery_label">Батерия</string>
<string name="device_settings_charge_cap_label">Оптимизиран лимит за зареждане</string>
<string name="device_settings_charge_cap_description">Научава ежедневието ви и спира зареждането около 80%%, за да удължи живота на батерията, като напълно зарежда слушалките преди да е вероятно да ги използвате.</string>
<string name="device_settings_charge_cap_rejected_message">Оптимизираният лимит за зареждане не можа да бъде променен. Това е експериментална функция — моля, докладвайте ако продължава да се проваля.</string>
<string name="device_settings_category_connections_label">Свързани устройства</string>
<string name="device_settings_connected_devices_description">Другите устройства, свързани в момента към тези AirPods</string>
<string name="device_settings_connected_device_label">Устройство %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">পটভূমি স্বচ্ছতা</string>
<string name="widget_config_show_device_label">ডিভাইসের নাম দেখান</string>
<string name="widget_config_reset_label">ডিফল্টে পুনরায় সেট করুন</string>
<string name="widget_config_preview_resize_hint">আপনি হোম স্ক্রিনে রাখার পরে উইজেটটি পুনরায় আকার দিতে পারবেন।</string>
<string name="widget_config_custom_label">কাস্টম</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">গাঢ়</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">এটি একটি অজানা ডিভাইস, তবে এটি অনুরূপ বার্তা বিন্যাস ব্যবহার করছে। চলুন এর জন্য সমর্থন যোগ করি, আমার সাথে যোগাযোগ করুন :)</string>
<string name="pods_none_label_short">কোনো ডিভাইস নেই</string>
<string name="pods_charging_label">চার্জ হচ্ছে</string>
<string name="pods_charging_optimized_label">অপ্টিমাইজড</string>
<string name="pods_inear_label">কানে</string>
<string name="pods_microphone_label">মাইক্রোফোন</string>
<string name="anc_mode_off">বন্ধ</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">ব্লুটুথ ডিভাইস লেবেল</string>
<string name="device_settings_info_model_label">মডেল</string>
<string name="device_settings_info_manufacturer_label">প্রস্তুতকারক</string>
<string name="device_settings_info_hardware_label">হার্ডওয়্যার</string>
<string name="device_settings_info_serial_label">সিরিয়াল নম্বর</string>
<string name="device_settings_info_firmware_label">ফার্মওয়্যার</string>
<string name="device_settings_info_firmware_pending_label">বিচারাধীন ফার্মওয়্যার</string>
<string name="device_settings_info_build_label">বিল্ড</string>
<string name="device_settings_info_left_serial_label">বাম পড সিরিয়াল</string>
<string name="device_settings_info_right_serial_label">ডান পড সিরিয়াল</string>
<string name="device_settings_info_left_bonded_label">বাম পেয়ার করা</string>
<string name="device_settings_info_right_bonded_label">ডান পেয়ার করা</string>
<string name="device_settings_info_details_label">ডিভাইস বিস্তারিত</string>
<string name="device_settings_info_details_action">ডিভাইস বিস্তারিত দেখুন</string>
<string name="device_settings_info_status_label">স্ট্যাটাস</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">এই ডিভাইসটি কাছাকাছি আছে কিন্তু এই ফোনে সংযুক্ত নয়। সেটিংস এবং নিয়ন্ত্রণে প্রবেশ করতে সংযুক্ত করুন।</string>
<string name="device_settings_not_connected_connect_action">সংযুক্ত করুন</string>
<string name="device_settings_not_connected_open_settings_action">ব্লুটুথ সেটিংস খুলুন</string>
<string name="device_settings_not_nearby_label">ডিভাইস কাছাকাছি নেই</string>
<string name="device_settings_not_nearby_description">সেটিংস শুধুমাত্র তখনই পাওয়া যাবে যখন এই ডিভাইসটি কাছাকাছি থাকবে এবং আপনার ফোনের সাথে সংযুক্ত থাকবে।</string>
<string name="device_settings_aap_unavailable_label">উন্নত সেটিংস অনুপলব্ধ</string>
<string name="device_settings_aap_unavailable_description">উন্নত AirPods সেটিংসের জন্য একটি ব্লুটুথ ফিচার প্রয়োজন যা এই ফোনে উপলব্ধ নয়। এটি আপনার ডিভাইস প্রস্তুতকারকের ভবিষ্যৎ Android আপডেট দ্বারা সমাধান হতে পারে।</string>
<string name="device_settings_aap_unavailable_action">সামঞ্জস্যতা ট্র্যাকার দেখুন</string>
<string name="device_settings_category_sound_label">শব্দ</string>
<string name="device_settings_category_controls_label">নিয়ন্ত্রণ</string>
<string name="device_settings_nc_one_airpod_label">একটি পড দিয়ে ANC</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">মাইক্রোফোন মিউট করতে একবার চাপুন</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">কল শেষ করতে দুবার চাপুন</string>
<string name="device_settings_open_cd">ডিভাইস সেটিংস খুলুন</string>
<string name="overview_card_missing_paired_device_cd">প্রোফাইলে কোনো পেয়ার করা ব্লুটুথ ডিভাইস নেই — প্রোফাইল সম্পাদনা করতে ট্যাপ করুন</string>
<string name="overview_card_missing_paired_device">এই প্রোফাইলে কোনো পেয়ার করা ব্লুটুথ ডিভাইস নেই</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">সাধারণ</string>
<string name="device_settings_microphone_mode_label">মাইক্রোফোন</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">অফকে একটি নির্বাচনযোগ্য নয়েজ কন্ট্রোল মোড হিসেবে অনুমতি দিন। অক্ষম থাকলে, স্টেম সাইক্লিং করার সময় অফ এড়িয়ে যায়।</string>
<string name="device_settings_sleep_detection_label">ঘুম শনাক্তি</string>
<string name="device_settings_sleep_detection_description">ঘুমিয়ে পড়লে স্বয়ংক্রিয়ভাবে অডিও বিরত করুন</string>
<string name="reaction_sleep_channel_label">ঘুম শনাক্তকরণ</string>
<string name="reaction_sleep_notification_title">ঘুম শনাক্তকরণ দ্বারা বিরতি দেওয়া হয়েছে</string>
<string name="reaction_sleep_notification_text">%1$s জানিয়েছে আপনি ঘুমিয়ে পড়েছেন, তাই আপনার সঙ্গীত বিরতি দেওয়া হয়েছে। আপনি ডিভাইস সেটিংসে এটি নিষ্ক্রিয় করতে পারেন।</string>
<string name="device_settings_rename_label">নাম বদলান</string>
<string name="device_settings_rename_hint">ডিভাইসের নাম</string>
<string name="device_settings_rename_confirm">নাম বদলান</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">ব্লুটুথ সেটিংস</string>
<string name="device_settings_send_failed">সেটিং প্রয়োগ করা যায়নি: %1$s</string>
<string name="device_settings_anc_off_rejected_message">এই ডিভাইসে অফ মোড সক্রিয় নেই। নয়েজ কন্ট্রোলের অধীনে \"অফ মোড অনুমতি দিন\" সক্রিয় করুন।</string>
<string name="device_settings_category_battery_label">ব্যাটারি</string>
<string name="device_settings_charge_cap_label">অপ্টিমাইজড চার্জ সীমা</string>
<string name="device_settings_charge_cap_description">আপনার রুটিন শিখে ব্যাটারি জীবন বাড়াতে প্রায় ৮0%% এ চার্জ বিরতি রাখে, আপনি ব্যবহার করার আগে পডসগুলি পূর্ণ চার্জ করে দেয়।</string>
<string name="device_settings_charge_cap_rejected_message">অপ্টিমাইজড চার্জ সীমা পরিবর্তন করা যায়নি। এটি একটি প্রযোগমূলক বৈশিষ্ট্য — বারবার ব্যর্থ হলে দয়া করে জানান।</string>
<string name="device_settings_category_connections_label">সংযুক্ত ডিভাইসগুলো</string>
<string name="device_settings_connected_devices_description">এই AirPodsডে বর্তমানে সংযুক্ত অন্যান্য ডিভাইস</string>
<string name="device_settings_connected_device_label">ডিভাইস %d</string>
+19 -3
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparència del fons</string>
<string name="widget_config_show_device_label">Mostra el nom del dispositiu</string>
<string name="widget_config_reset_label">Restaura els valors per defecte</string>
<string name="widget_config_preview_resize_hint">Podeu canviar la mida del giny després de col·locar-lo a la pantalla d\'inici.</string>
<string name="widget_config_custom_label">Personalitzat</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Fosc</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Aquest és un dispositiu desconegut, però utilitza un format de missatge similar. Anem a fer-lo compatible, contacteu amb mi :)</string>
<string name="pods_none_label_short">Cap dispositiu</string>
<string name="pods_charging_label">Carregant</string>
<string name="pods_charging_optimized_label">Optimitzada</string>
<string name="pods_inear_label">A l\'orella</string>
<string name="pods_microphone_label">Micròfon</string>
<string name="anc_mode_off">Desactivat</string>
@@ -292,7 +294,7 @@
<string name="profiles_delete_action">Elimina</string>
<string name="profiles_basic_info_title">Informació del dispositiu</string>
<string name="profiles_basic_info_description">Configureu el nom del dispositiu, el model i l\'emparellament Bluetooth opcional.</string>
<string name="profiles_signal_quality_title">Qualitat de senyal mínima</string>
<string name="profiles_signal_quality_title">Qualitat de senyal màxima</string>
<string name="profiles_signal_quality_description">Només es detecten dispositius amb una intensitat de senyal superior a aquest llindar. Els valors més baixos augmenten el rang de detecció, però poden causar falsos positius. No configureu aquest valor massa alt: la recepció Bluetooth generalment és deficient i varia segons la distància i els obstacles.</string>
<string name="profiles_identitykey_label">Clau d\'identitat</string>
<string name="profilessettings_maindevice_identitykey_description">La clau de resolució d\'identitat (IRK) del dispositiu ajuda al CAPod a identificar-lo entre els dispositius propers.</string>
@@ -394,7 +396,7 @@
<string name="support_contact_send_action">Obre l\'aplicació de correu</string>
<string name="support_contact_no_email_app">No s\'ha trobat cap aplicació de correu en aquest dispositiu.</string>
<string name="support_contact_debuglog_delete_title">Voleu suprimir el registre de depuració?</string>
<string name="support_contact_debuglog_delete_message">Aquest registre de depuració se suprimirà permanentment.</string>
<string name="support_contact_debuglog_delete_message">Aquest registre de depuració se suprimrirà permanentment.</string>
<!-- Post-share confirmation -->
<string name="support_debuglog_sent_title">S\'ha enviat el registre?</string>
<string name="support_debuglog_sent_message">El registre de depuració s\'ha enviat correctament? El registre s\'eliminarà.</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Etiqueta del dispositiu Bluetooth</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">Fabricant</string>
<string name="device_settings_info_hardware_label">Maquinari</string>
<string name="device_settings_info_serial_label">Número de sèrie</string>
<string name="device_settings_info_firmware_label">Microprogramari</string>
<string name="device_settings_info_firmware_pending_label">Microprogramari pendent</string>
<string name="device_settings_info_build_label">Compilació</string>
<string name="device_settings_info_left_serial_label">Número de sèrie de l\'auricular esquerre</string>
<string name="device_settings_info_right_serial_label">Número de sèrie de l\'auricular dret</string>
<string name="device_settings_info_left_bonded_label">Esquerre vinculat</string>
<string name="device_settings_info_right_bonded_label">Dret vinculat</string>
<string name="device_settings_info_details_label">Detalls del dispositiu</string>
<string name="device_settings_info_details_action">Mostra els detalls del dispositiu</string>
<string name="device_settings_info_status_label">Estat</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Aquest dispositiu és a prop però no està connectat a aquest telèfon. Connecteu-lo per accedir a la configuració i als controls.</string>
<string name="device_settings_not_connected_connect_action">Connecta</string>
<string name="device_settings_not_connected_open_settings_action">Obre la configuració del Bluetooth</string>
<string name="device_settings_not_nearby_label">Dispositiu no proper</string>
<string name="device_settings_not_nearby_description">La configuració només està disponible quan aquest dispositiu està a prop i connectat al telèfon.</string>
<string name="device_settings_aap_unavailable_label">Configuració avançada no disponible</string>
<string name="device_settings_aap_unavailable_description">La configuració avançada dels AirPods requereix una funció de Bluetooth que no està disponible en aquest telèfon. Pot ser que una actualització futura d\'Android del fabricant del dispositiu ho resolgui.</string>
<string name="device_settings_aap_unavailable_action">Mostra el rastrejador de compatibilitat</string>
<string name="device_settings_category_sound_label">So</string>
<string name="device_settings_category_controls_label">Controls</string>
<string name="device_settings_nc_one_airpod_label">ANC amb un sol auricular</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Un sol toc per silenciar el micròfon</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Doble toc per acabar la trucada</string>
<string name="device_settings_open_cd">Obre la configuració del dispositiu</string>
<string name="overview_card_missing_paired_device_cd">El perfil no té cap dispositiu Bluetooth emparellat: toqueu per editar el perfil</string>
<string name="overview_card_missing_paired_device">Aquest perfil no té cap dispositiu Bluetooth emparellat.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">General</string>
<string name="device_settings_microphone_mode_label">Micròfon</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Permet l\'opció Desactivat com a mode de control de soroll seleccionable. Quan està desactivat, les pliques ometen l\'opció Desactivat durant el cicle.</string>
<string name="device_settings_sleep_detection_label">Detecció del son</string>
<string name="device_settings_sleep_detection_description">Pausa automàticament l\'àudio quan us adormiu</string>
<string name="reaction_sleep_channel_label">Detecció del son</string>
<string name="reaction_sleep_notification_title">Pausat per la detecció del son</string>
<string name="reaction_sleep_notification_text">%1$s ha informat que us heu adormit, de manera que la vostra música s\'ha posat en pausa. Podeu desactivar-ho a la configuració del dispositiu.</string>
<string name="device_settings_rename_label">Canvia el nom</string>
<string name="device_settings_rename_hint">Nom del dispositiu</string>
<string name="device_settings_rename_confirm">Canvia el nom</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Configuració del Bluetooth</string>
<string name="device_settings_send_failed">No s\'ha pogut aplicar la configuració: %1$s</string>
<string name="device_settings_anc_off_rejected_message">El mode Desactivat no esta habilitat en aquest dispositiu. Activeu l\'opció «Permet el mode desactivat» al Control de soroll.</string>
<string name="device_settings_category_battery_label">Bateria</string>
<string name="device_settings_charge_cap_label">Límit de càrrega optimitzat</string>
<string name="device_settings_charge_cap_description">Aprèn la vostra rutina i pausa la càrrega al voltant del 80%% per allargar la vida de la bateria i completa la càrrega dels auriculars abans que els necessiteu.</string>
<string name="device_settings_charge_cap_rejected_message">No s\'ha pogut canviar el límit de càrrega optimitzat. Aquesta és una funció experimental; informeu si continua fallant.</string>
<string name="device_settings_category_connections_label">Dispositius connectats</string>
<string name="device_settings_connected_devices_description">Altres dispositius connectats ara a aquests AirPods</string>
<string name="device_settings_connected_device_label">Dispositiu %d</string>
+33 -17
View File
@@ -26,7 +26,7 @@
<string name="upgrade_benefit_popups">Vyskakovací okno při otevření a připojení</string>
<string name="upgrade_benefit_widgets">Widgety domovské obrazovky</string>
<string name="upgrade_benefit_device_settings">Pokročilá nastavení zařízení</string>
<string name="upgrade_benefit_device_controls">Ovládání zařízení &#8212; hluk, stonky &amp; více</string>
<string name="upgrade_benefit_device_controls">Ovládací prvky zařízení &#8212; šum, okolní hluk a další</string>
<string name="upgrade_benefit_support">Podpořit vývojáře</string>
<string name="upgrade_benefit_disclaimer">Dostupnost funkcí závisí na vašich sluchátkách a zařízení.</string>
<string name="upgrade_screen_options_description">Stejné funkce, různé ceny. Předplatné zahrnuje bezplatnou zkušební dobu a lze jej kdykoli zrušit.</string>
@@ -82,10 +82,10 @@
<string name="settings_popup_connected_label">Zobrazit vyskakovací okno při připojení</string>
<string name="settings_popup_connected_description">Zobrazit vyskakovací okno při prvním připojení zařízení.</string>
<string name="settings_popup_info_not_in_app">Vyskakovací okna jsou zobrazována pouze tehdy, když nejste v aplikaci.</string>
<string name="settings_popup_warning_manual_mode">Vyskakovací okna vyžadují, aby monitor běžel. Váš režim monitoru je nastaven na Když je aplikace otevřena, což zastaví monitor, když aplikaci opustíte.</string>
<string name="settings_popup_warning_manual_mode">Vyskakovací okna vyžadují, aby monitor běžel. Váš režim monitoru je nastaven na \"Když je aplikace otevřena\", což zastaví monitor, když aplikaci opustíte.</string>
<string name="general_fix_it_action">Opravit</string>
<string name="device_settings_experimental_title">Experimentální funkce</string>
<string name="device_settings_experimental_description">Tato funkce ještě nebyla testována na všech zařízeních. Pokud něco nefunguje správně, otevřete prosím problém s protokolem ladění.</string>
<string name="device_settings_experimental_description">Tato funkce ještě nebyla testována na všech zařízeních. Pokud něco nefunguje správně, nahlaste problém s logem ladění.</string>
<string name="device_settings_experimental_action">Otevřít sledovač problémů</string>
<string name="notification_channel_device_status_label">Stav zařízení</string>
<string name="notification_channel_device_status_connected_label">Připojené zařízení</string>
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Průhlednost pozadí</string>
<string name="widget_config_show_device_label">Zobrazit název zařízení</string>
<string name="widget_config_reset_label">Obnovit výchozí nastavení</string>
<string name="widget_config_preview_resize_hint">Velikost widgetu můžete změnit po jeho umístění na domovskou obrazovku.</string>
<string name="widget_config_custom_label">Vlastní</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Tmavá</string>
@@ -191,7 +192,7 @@
<string name="overview_nomaindevice_label">Není nakonfigurováno žádné zařízení</string>
<string name="overview_nomaindevice_description">Nakonfigurujte zařízení tak, aby začalo sledovat stav baterie a aktivujte další funkce.</string>
<string name="overview_bluetooth_disabled_icon_cd">Bluetooth vypnuto, otevřít nastavení</string>
<string name="overview_bluetooth_nearby_icon_cd">Zařízení v blízkosti, otevřít nastavení Bluetooth</string>
<string name="overview_bluetooth_nearby_icon_cd">Zařízení v blízkösti, otevřít nastavení Bluetooth</string>
<string name="overview_bluetooth_connected_icon_cd">Zařízení připojeno, otevřít nastavení Bluetooth</string>
<string name="overview_bluetooth_disabled_label">Bluetooth je vypnuto</string>
<string name="overview_bluetooth_disabled_description">Bluetooth je vypnuto, zapněte jej ;)</string>
@@ -205,10 +206,10 @@
<item quantity="other">%d zařízení bez odpovídajícího profilu</item>
</plurals>
<plurals name="overview_more_devices_upgrade">
<item quantity="one">o %d zařízení více</item>
<item quantity="few">o %d zařízení více</item>
<item quantity="many">o %d zařízení více</item>
<item quantity="other">o %d zařízení více</item>
<item quantity="one">%d další zařízení</item>
<item quantity="few">%d další zařízení</item>
<item quantity="many">%d dalších zařízení</item>
<item quantity="other">%d dalších zařízení</item>
</plurals>
<string name="permission_bluetooth_connect_label">Připojení Bluetooth</string>
<string name="permission_bluetooth_connect_description">Tato aplikace vyžaduje oprávnění \"připojení Bluetooth\" pro interakci se spárovanými zařízeními a zahájení připojení.</string>
@@ -254,6 +255,7 @@
<string name="pods_unknown_contact_dev">Jedná se o neznámé zařízení, ale používá podobný formát zprávy. Přidejme pro něj podporu. Kontaktujte mě :)</string>
<string name="pods_none_label_short">Žádné zařízení</string>
<string name="pods_charging_label">Nabíjení</string>
<string name="pods_charging_optimized_label">Optimalizováno</string>
<string name="pods_inear_label">V uchu</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Vypnuto</string>
@@ -266,7 +268,7 @@
<string name="signal_badge_key_encrypted_cd">Šifrované připojení</string>
<string name="signal_badge_aap_cd">Přímé připojení aktivní</string>
<string name="signal_indicator_bars_cd">Síla signálu: %1$d ze 4 dílků</string>
<string name="signal_indicator_disconnected_cd">Signál: odpojen</string>
<string name="signal_indicator_disconnected_cd">Signál: odpojeno</string>
<string name="pods_yours">Vaše</string>
<string name="headset_being_worn_label">Používá se</string>
<string name="headset_not_being_worn_label">Nepoužíváno</string>
@@ -417,11 +419,15 @@
<string name="device_settings_info_bt_name_label">Název zařízení Bluetooth</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">Výrobce</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Sériové číslo</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Čekající firmware</string>
<string name="device_settings_info_build_label">Sestavení</string>
<string name="device_settings_info_left_serial_label">Sériové číslo levého sluchátka</string>
<string name="device_settings_info_right_serial_label">Sériové číslo pravého sluchátka</string>
<string name="device_settings_info_left_bonded_label">Levá spárována</string>
<string name="device_settings_info_right_bonded_label">Pravá spárována</string>
<string name="device_settings_info_details_label">Detaily zařízení</string>
<string name="device_settings_info_details_action">Zobrazit detaily zařízení</string>
<string name="device_settings_info_status_label">Stav</string>
@@ -431,8 +437,11 @@
<string name="device_settings_not_connected_description">Toto zařízení je v dosahu, ale není připojeno k tomuto telefonu. Připojte se pro přístup k nastavení a ovládacím prvkům.</string>
<string name="device_settings_not_connected_connect_action">Připojit</string>
<string name="device_settings_not_connected_open_settings_action">Otevřít nastavení Bluetooth</string>
<string name="device_settings_aap_unavailable_label">Rozšířená nastavení nedostupná</string>
<string name="device_settings_not_nearby_label">Zařízení není v blízkosti</string>
<string name="device_settings_not_nearby_description">Nastavení jsou dostupná pouze tehdy, když je toto zařízení v blízkosti a připojeno k vašemu telefonu.</string>
<string name="device_settings_aap_unavailable_label">Pokročilá nastavení nedostupná</string>
<string name="device_settings_aap_unavailable_description">Pokročilá nastavení AirPods vyžadují funkci Bluetooth, která není na tomto telefonu dostupná. To může být vyřešeno budoucí aktualizací Androidu od výrobce vašeho zařízení.</string>
<string name="device_settings_aap_unavailable_action">Zobrazit sledování kompatibility</string>
<string name="device_settings_category_sound_label">Zvuk</string>
<string name="device_settings_category_controls_label">Ovládání</string>
<string name="device_settings_nc_one_airpod_label">ANC s jedním sluchátkem</string>
@@ -440,7 +449,7 @@
<string name="device_settings_personalized_volume_label">Personalizovaná hlasitost</string>
<string name="device_settings_personalized_volume_description">Upravit hlasitost médií na základě prostředí</string>
<string name="device_settings_conversation_awareness_description">Snížit hlasitost médií a utlumit hluk, když k vám někdo mluví</string>
<string name="device_settings_tone_volume_label">Hlasitost zvěnkě</string>
<string name="device_settings_tone_volume_label">Hlasitost zvonění</string>
<string name="device_settings_tone_volume_description">Hlasitost potvrzení a vyzvánění</string>
<string name="device_settings_adaptive_noise_label">Adaptivní potlačení hluku</string>
<string name="device_settings_adaptive_noise_description">Kolik okolního hluku je propouštěno</string>
@@ -470,7 +479,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Jednorázový stisk pro ztlumení mikrofonu</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Dvojitý stisk pro ukončení hovoru</string>
<string name="device_settings_open_cd">Otevřít nastavení zařízení</string>
<string name="overview_card_missing_paired_device_cd">Profil nemá spárované Bluetooth zařízení — klepnutím upravte profil</string>
<string name="overview_card_missing_paired_device">Tento profil nemá spárované zařízení Bluetooth.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Obecné</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -493,6 +502,9 @@
<string name="device_settings_allow_off_description">Povolit Vypnuto jako volitelný režim kontroly hluku. Pokud je zakázáno, stopky při cyklování přeskočí Vypnuto.</string>
<string name="device_settings_sleep_detection_label">Detekce spánku</string>
<string name="device_settings_sleep_detection_description">Automaticky pozastavit zvuk, když usnete</string>
<string name="reaction_sleep_channel_label">Detekce spánku</string>
<string name="reaction_sleep_notification_title">Pozastaveno detekcí spánku</string>
<string name="reaction_sleep_notification_text">%1$s oznámil, že jste usnuli, takže vaše hudba byla pozastavena. Toto můžete vypnout v nastavení zařízení.</string>
<string name="device_settings_rename_label">Přejmenovat</string>
<string name="device_settings_rename_hint">Název zařízení</string>
<string name="device_settings_rename_confirm">Přejmenovat</string>
@@ -502,13 +514,17 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Nastavení Bluetooth</string>
<string name="device_settings_send_failed">Nastavení nelze použít: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Režim Vypnuto není na tomto zařízení povolen. Povolte „Povolit režim Vypnuto“ v části Kontrola hluku.</string>
<string name="device_settings_category_battery_label">Baterie</string>
<string name="device_settings_charge_cap_label">Optimalizovaný limit nabíjení</string>
<string name="device_settings_charge_cap_description">Naučí se vaše návyky a pozastaví nabíjení přibližně na 80 %%, aby prodloužilo životnost baterie, a dobíjí sluchátka těsně před jejich použitím.</string>
<string name="device_settings_charge_cap_rejected_message">Optimalizovaný limit nabíjení nepšel změnit. Jde o experimentalní funkci — prosím nahláste, pokud to nefunguje opakovaně.</string>
<string name="device_settings_category_connections_label">Připojená zařízení</string>
<string name="device_settings_connected_devices_description">Další zařízení aktuálně připojená k těmto AirPods</string>
<string name="device_settings_connected_device_label">Zařízení %d</string>
<string name="device_settings_connected_device_call">Probíhá hovor</string>
<string name="device_settings_connected_device_media">Přehrává média</string>
<string name="device_settings_noise_control_label">Ovládání zvuku</string>
<string name="device_settings_eq_label">Ekvualizér</string>
<string name="device_settings_eq_label">Ekvalizér</string>
<!-- Press Controls -->
<string name="press_controls_title">Ovládání stisku</string>
<string name="press_controls_nav_description">Nastavte chování stisku a přiřaďte stisky k akcím</string>
@@ -519,13 +535,13 @@
<string name="press_controls_long_press">Dlouhý stisk</string>
<string name="press_controls_long_press_anc_cycle_info">Přiřazení akce dlouhého stisku také zakáže cyklování režimů Kontroly hluku (např. Potlačení hluku, Transparentnost) stisknutím a podržením AirPods.</string>
<string name="press_controls_mappings_section_title">Mapování stisku</string>
<string name="press_controls_left">Levý</string>
<string name="press_controls_right">Pravý</string>
<string name="press_controls_left">Levé</string>
<string name="press_controls_right">Pravé</string>
<string name="press_action_none">Výchozí</string>
<string name="press_action_no_action">Žádná akce</string>
<string name="press_action_play_pause">Přehrát/Pozastavit</string>
<string name="press_action_next_track">Další stopa</string>
<string name="press_action_previous_track">Předchozí stopa</string>
<string name="press_action_next_track">Další skladba</string>
<string name="press_action_previous_track">Předchozí skladba</string>
<string name="press_action_volume_up">Hlasitost nahoru</string>
<string name="press_action_volume_down">Hlasitost dolů</string>
<string name="press_action_mute_toggle">Ztlumit hudbu</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Baggrundsgennemsigtighed</string>
<string name="widget_config_show_device_label">Vis enhedsnavn</string>
<string name="widget_config_reset_label">Nulstil til standard</string>
<string name="widget_config_preview_resize_hint">Du kan ændre størrelsen på widgetten, efter du har placeret den på din startskærm.</string>
<string name="widget_config_custom_label">Brugerdefineret</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Mørk</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Dette er en ukendt enhed, men den bruger et lignende meddelelsesformat. Lad os tilføje understøttelse for den, kontakt mig :)</string>
<string name="pods_none_label_short">Ingen enhed</string>
<string name="pods_charging_label">Oplader</string>
<string name="pods_charging_optimized_label">Optimeret</string>
<string name="pods_inear_label">I øret</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Fra</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth-enhedsnavn</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">Producent</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Serienummer</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Afventende firmware</string>
<string name="device_settings_info_build_label">Version</string>
<string name="device_settings_info_left_serial_label">Venstre pod-serienummer</string>
<string name="device_settings_info_right_serial_label">Højre pod-serienummer</string>
<string name="device_settings_info_left_bonded_label">Venstre parret</string>
<string name="device_settings_info_right_bonded_label">Højre parret</string>
<string name="device_settings_info_details_label">Enhedsdetaljer</string>
<string name="device_settings_info_details_action">Vis enhedsdetaljer</string>
<string name="device_settings_info_status_label">Status</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Denne enhed er i nærheden, men ikke forbundet til denne telefon. Opret forbindelse for at få adgang til indstillinger og kontrolfunktioner.</string>
<string name="device_settings_not_connected_connect_action">Opret forbindelse</string>
<string name="device_settings_not_connected_open_settings_action">Åbn Bluetooth-indstillinger</string>
<string name="device_settings_not_nearby_label">Enhed ikke i nærheden</string>
<string name="device_settings_not_nearby_description">Indstillinger er kun tilgængelige, når denne enhed er i nærheden og tilsluttet din telefon.</string>
<string name="device_settings_aap_unavailable_label">Avancerede indstillinger er ikke tilgængelige</string>
<string name="device_settings_aap_unavailable_description">Avancerede AirPods-indstillinger kræver en Bluetooth-funktion, der ikke er tilgængelig på denne telefon. Dette kan muligvis løses af en fremtidig Android-opdatering fra din enhedsproducent.</string>
<string name="device_settings_aap_unavailable_action">Se kompatibilitetssporing</string>
<string name="device_settings_category_sound_label">Lyd</string>
<string name="device_settings_category_controls_label">Betjeninger</string>
<string name="device_settings_nc_one_airpod_label">ANC med én pod</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Enkelt tryk for at slå mikrofon fra</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Dobbelt tryk for at afslutte opkald</string>
<string name="device_settings_open_cd">Åbn enhedsindstillinger</string>
<string name="overview_card_missing_paired_device_cd">Profilen har ingen parret Bluetooth-enhed — tryk for at redigere profilen</string>
<string name="overview_card_missing_paired_device">Denne profil har ingen parret Bluetooth-enhed.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Generel</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Tillad Slukket som en valgbar støjkontrol-tilstand. Når deaktiveret, springer stammer Slukket over under cyklus.</string>
<string name="device_settings_sleep_detection_label">Søvnregistrering</string>
<string name="device_settings_sleep_detection_description">Sæt automatisk lyd på pause når du falder i søvn</string>
<string name="reaction_sleep_channel_label">Søvnregistrering</string>
<string name="reaction_sleep_notification_title">Sat på pause af søvnregistrering</string>
<string name="reaction_sleep_notification_text">%1$s registrerede, at du faldt i søvn, så din musik blev sat på pause. Du kan deaktivere dette i enhedsindstillingerne.</string>
<string name="device_settings_rename_label">Omdøb</string>
<string name="device_settings_rename_hint">Enhedsnavn</string>
<string name="device_settings_rename_confirm">Omdøb</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth-indstillinger</string>
<string name="device_settings_send_failed">Kunne ikke anvende indstilling: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Slukket-tilstand er ikke aktiveret på denne enhed. Aktivér \"Tillad Slukket-tilstand\" under Støjkontrol.</string>
<string name="device_settings_category_battery_label">Batteri</string>
<string name="device_settings_charge_cap_label">Optimeret opladningsgrænse</string>
<string name="device_settings_charge_cap_description">Lær din rutine og sæt opladningen på pause ved ca. 80%% for at forlænge batterilevetiden, og top pods op, inden du sandsynligvis skal bruge dem.</string>
<string name="device_settings_charge_cap_rejected_message">Optimeret opladningsgrænse kunne ikke ændres. Dette er en eksperimentel funktion — rapporter venligst, hvis det bliver ved med at fejle.</string>
<string name="device_settings_category_connections_label">Tilsluttede enheder</string>
<string name="device_settings_connected_devices_description">Andre enheder der i øjeblikket er tilsluttet disse AirPods</string>
<string name="device_settings_connected_device_label">Enhed %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Hintergrundtransparenz</string>
<string name="widget_config_show_device_label">Gerätenamen anzeigen</string>
<string name="widget_config_reset_label">Auf Standardwerte zurücksetzen</string>
<string name="widget_config_preview_resize_hint">Du kannst die Widget-Größe anpassen, nachdem du es auf deinem Startbildschirm platziert hast.</string>
<string name="widget_config_custom_label">Benutzerdefiniert</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Dunkel</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Dies ist ein unbekanntes Gerät, aber es verwendet ein ähnliches Nachrichtenformat. Lass uns Unterstützung dafür hinzufügen, kontaktiere mich :)</string>
<string name="pods_none_label_short">Kein Gerät</string>
<string name="pods_charging_label">Laden</string>
<string name="pods_charging_optimized_label">Optimiert</string>
<string name="pods_inear_label">Im Ohr</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Aus</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth-Gerätebezeichnung</string>
<string name="device_settings_info_model_label">Modell</string>
<string name="device_settings_info_manufacturer_label">Hersteller</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Seriennummer</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Ausstehende Firmware</string>
<string name="device_settings_info_build_label">Build</string>
<string name="device_settings_info_left_serial_label">Seriennummer linker Pod</string>
<string name="device_settings_info_right_serial_label">Seriennummer rechter Pod</string>
<string name="device_settings_info_left_bonded_label">Links gekoppelt</string>
<string name="device_settings_info_right_bonded_label">Rechts gekoppelt</string>
<string name="device_settings_info_details_label">Gerätedetails</string>
<string name="device_settings_info_details_action">Gerätedetails anzeigen</string>
<string name="device_settings_info_status_label">Status</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Dieses Gerät ist in der Nähe, aber nicht mit diesem Telefon verbunden. Verbinden, um auf Einstellungen und Steuerungen zuzugreifen.</string>
<string name="device_settings_not_connected_connect_action">Verbinden</string>
<string name="device_settings_not_connected_open_settings_action">Bluetooth-Einstellungen öffnen</string>
<string name="device_settings_not_nearby_label">Gerät nicht in der Nähe</string>
<string name="device_settings_not_nearby_description">Einstellungen sind nur verfügbar, wenn dieses Gerät in der Nähe und mit deinem Telefon verbunden ist.</string>
<string name="device_settings_aap_unavailable_label">Erweiterte Einstellungen nicht verfügbar</string>
<string name="device_settings_aap_unavailable_description">Erweiterte AirPods-Einstellungen erfordern eine Bluetooth-Funktion, die auf diesem Telefon nicht verfügbar ist. Dies könnte durch ein zukünftiges Android-Update deines Geräteherstellers behoben werden.</string>
<string name="device_settings_aap_unavailable_action">Kompatibilitätsliste anzeigen</string>
<string name="device_settings_category_sound_label">Ton</string>
<string name="device_settings_category_controls_label">Steuerung</string>
<string name="device_settings_nc_one_airpod_label">ANC mit einem Pod</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Einmaliges Drücken zum Stummschalten des Mikrofons</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Zweimaliges Drücken zum Beenden des Anrufs</string>
<string name="device_settings_open_cd">Geräteeinstellungen öffnen</string>
<string name="overview_card_missing_paired_device_cd">Profil hat kein gekoppeltes Bluetooth-Gerät — tippe zum Bearbeiten des Profils</string>
<string name="overview_card_missing_paired_device">Dieses Profil hat kein gekoppeltes Bluetooth-Gerät.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Allgemein</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Aus als auswählbaren Geräuschsteuerungsmodus erlauben. Wenn deaktiviert, überspringen die Stiele den Aus-Modus beim Durchschalten.</string>
<string name="device_settings_sleep_detection_label">Schlaferkennung</string>
<string name="device_settings_sleep_detection_description">Audio automatisch pausieren, wenn du einschläfst</string>
<string name="reaction_sleep_channel_label">Schlaferkennung</string>
<string name="reaction_sleep_notification_title">Pausiert durch Schlafmodus-Erkennung</string>
<string name="reaction_sleep_notification_text">%1$s hat erkannt, dass du eingeschlafen bist, daher wurde deine Musik pausiert. Du kannst dies in den Geräteeinstellungen deaktivieren.</string>
<string name="device_settings_rename_label">Umbenennen</string>
<string name="device_settings_rename_hint">Gerätename</string>
<string name="device_settings_rename_confirm">Umbenennen</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth-Einstellungen</string>
<string name="device_settings_send_failed">Einstellung konnte nicht angewendet werden: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Der Aus-Modus ist auf diesem Gerät nicht aktiviert. Aktiviere „Aus-Modus erlauben“ unter Geräuschsteuerung.</string>
<string name="device_settings_category_battery_label">Akku</string>
<string name="device_settings_charge_cap_label">Optimiertes Ladelimit</string>
<string name="device_settings_charge_cap_description">Lernt deine Routine und pausiert das Laden bei ca. 80%%, um die Akkulebensdauer zu verlängern, und lädt die Pods vor der wahrscheinlichen Nutzung voll auf.</string>
<string name="device_settings_charge_cap_rejected_message">Das optimierte Ladelimit konnte nicht geändert werden. Dies ist eine experimentelle Funktion bitte melde es, wenn es weiterhin fehlschlägt.</string>
<string name="device_settings_category_connections_label">Verbundene Geräte</string>
<string name="device_settings_connected_devices_description">Andere Geräte, die derzeit mit diesen AirPods verbunden sind</string>
<string name="device_settings_connected_device_label">Gerät %d</string>
+19 -3
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Διαφάνεια φόντου</string>
<string name="widget_config_show_device_label">Εμφάνιση ονόματος συσκευής</string>
<string name="widget_config_reset_label">Επαναφορά στις προεπιλογές</string>
<string name="widget_config_preview_resize_hint">Μπορείτε να αλλάξετε το μέγεθος του widget αφού το τοποθετήσετε στην αρχική οθόνη σας.</string>
<string name="widget_config_custom_label">Προσαρμοσμένη</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Σκοτεινό</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Αυτή είναι μια άγνωστη συσκευή, αλλά χρησιμοποιεί παρόμοια μορφή μηνύματος. Ας προσθέσουμε υποστήριξη γι\' αυτήν, επικοινωνήστε μαζί μου :)</string>
<string name="pods_none_label_short">Καμία συσκευή</string>
<string name="pods_charging_label">Φόρτιση</string>
<string name="pods_charging_optimized_label">Βελτιστοποιημένη</string>
<string name="pods_inear_label">Στο αυτί</string>
<string name="pods_microphone_label">Μικρόφωνο</string>
<string name="anc_mode_off">Απενεργοποιημένο</string>
@@ -304,7 +306,7 @@
<string name="profiles_key_expected_format">Αναμενόμενη μορφή: %1$s</string>
<string name="profiles_key_status_configured">Διαμορφωμένο</string>
<string name="profiles_key_status_not_set">Μη ορισμένο</string>
<string name="profiles_signal_quality_min_label">Απενεργοποιημένο</string>
<string name="profiles_signal_quality_min_label">Απενεργο</string>
<string name="profiles_signal_quality_max_label">Αυστηρό</string>
<string name="profiles_priority_hint">Η σειρά των προφίλ καθορίζει την προτεραιότητα. Σύρετε τα προφίλ για αναδιάταξη - τα προφίλ υψηλότερα στη λίστα έχουν προτεραιότητα όταν ταιριάζουν πολλαπλές συσκευές.</string>
<!-- Theme Mode -->
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Ετικέτα Συσκευής Bluetooth</string>
<string name="device_settings_info_model_label">Μοντέλο</string>
<string name="device_settings_info_manufacturer_label">Κατασκευαστής</string>
<string name="device_settings_info_hardware_label">Υλικό</string>
<string name="device_settings_info_serial_label">Σειριακός Αριθμός</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Εκκρεμές Firmware</string>
<string name="device_settings_info_build_label">Έκδοση</string>
<string name="device_settings_info_left_serial_label">Σειριακός Αριθμός Αριστερού Pod</string>
<string name="device_settings_info_right_serial_label">Σειριακός Αριθμός Δεξιού Pod</string>
<string name="device_settings_info_left_bonded_label">Αριστερό Συζευγμένο</string>
<string name="device_settings_info_right_bonded_label">Δεξί Συζευγμένο</string>
<string name="device_settings_info_details_label">Λεπτομέρειες Συσκευής</string>
<string name="device_settings_info_details_action">Εμφάνιση λεπτομερειών συσκευής</string>
<string name="device_settings_info_status_label">Κατάσταση</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Αυτή η συσκευή είναι κοντά αλλά δεν είναι συνδεδεμένη σε αυτό το τηλέφωνο. Συνδεθείτε για πρόσβαση στις ρυθμίσεις και τις επιλογές ελέγχου.</string>
<string name="device_settings_not_connected_connect_action">Σύνδεση</string>
<string name="device_settings_not_connected_open_settings_action">Άνοιγμα Ρυθμίσεων Bluetooth</string>
<string name="device_settings_not_nearby_label">Η συσκευή δεν βρίσκεται κοντά</string>
<string name="device_settings_not_nearby_description">Οι ρυθμίσεις είναι διαθέσιμες μόνο όταν αυτή η συσκευή βρίσκεται κοντά και είναι συνδεδεμένη στο τηλέφωνό σας.</string>
<string name="device_settings_aap_unavailable_label">Οι προηγμένες ρυθμίσεις δεν είναι διαθέσιμες</string>
<string name="device_settings_aap_unavailable_description">Οι προηγμένες ρυθμίσεις AirPods απαιτούν λειτουργία Bluetooth που δεν είναι διαθέσιμη σε αυτό το τηλέφωνο. Αυτό μπορεί να επιλυθεί με μελλοντική ενημέρωση Android από τον κατασκευαστή της συσκευής σας.</string>
<string name="device_settings_aap_unavailable_action">Προβολή εργαλείου παρακολούθησης συμβατότητας</string>
<string name="device_settings_category_sound_label">Ήχος</string>
<string name="device_settings_category_controls_label">Χειριστήρια</string>
<string name="device_settings_nc_one_airpod_label">ANC με ένα pod</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Μία πίεση για σίγαση μικροφώνου</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Διπλή πίεση για τερματισμό κλήσης</string>
<string name="device_settings_open_cd">Άνοιγμα ρυθμίσεων συσκευής</string>
<string name="overview_card_missing_paired_device_cd">Το προφίλ δεν έχει συζευγμένη συσκευή Bluetooth — πατήστε για επεξεργασία προφίλ</string>
<string name="overview_card_missing_paired_device">Αυτό το προφίλ δεν έχει συζευγμένη συσκευή Bluetooth.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Γενικά</string>
<string name="device_settings_microphone_mode_label">Μικρόφωνο</string>
@@ -481,8 +490,11 @@
<string name="device_settings_listening_mode_cycle_adaptive">Προσαρμοστικό</string>
<string name="device_settings_allow_off_label">Να επιτρέπεται η λειτουργία Απενεργοποίησης</string>
<string name="device_settings_allow_off_description">Να επιτρέπεται η Απενεργοποίηση ως επιλέξιμη λειτουργία ελέγχου θορύβου. Όταν είναι απενεργοποιημένη, τα στελέχη παραλείπουν την Απενεργοποίηση κατά τον κύκλο.</string>
<string name="device_settings_sleep_detection_label">Ανίχνευση ύπνου</string>
<string name="device_settings_sleep_detection_label">Ανίχνευση ΍πνου</string>
<string name="device_settings_sleep_detection_description">Αυτόματη παύση ήχου όταν αποκοιμηθείτε</string>
<string name="reaction_sleep_channel_label">Ανίχνευση Ύπνου</string>
<string name="reaction_sleep_notification_title">Σε παύση από την Ανίχνευση Ύπνου</string>
<string name="reaction_sleep_notification_text">%1$s ανέφερε ότι αποκοιμηθήκατε, οπότε η μουσική σας τέθηκε σε παύση. Μπορείτε να το απενεργοποιήσετε στις ρυθμίσεις συσκευής.</string>
<string name="device_settings_rename_label">Μετονομασία</string>
<string name="device_settings_rename_hint">Όνομα συσκευής</string>
<string name="device_settings_rename_confirm">Μετονομασία</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Ρυθμίσεις Bluetooth</string>
<string name="device_settings_send_failed">Δεν ήταν δυνατή η εφαρμογή ρύθμισης: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Η λειτουργία Απενεργοποίησης δεν είναι ενεργοποιημένη σε αυτή τη συσκευή. Ενεργοποιήστε «Να επιτρέπεται η λειτουργία Απενεργοποίησης» στον Έλεγχο Θορύβου.</string>
<string name="device_settings_category_battery_label">Μπαταρία</string>
<string name="device_settings_charge_cap_label">Βελτιστοποιημένο Όριο Φόρτισης</string>
<string name="device_settings_charge_cap_description">Μαθαίνει τη ρουτίνα σας και διακόπτει τη φόρτιση γύρω στο 80%% για να παρατείνει τη διάρκεια ζωής της μπαταρίας, φορτίζοντας πλήρως τα pods πριν τα χρησιμοποιήσετε.</string>
<string name="device_settings_charge_cap_rejected_message">Δεν ήταν δυνατή η αλλαγή του Βελτιστοποιημένου Ορίου Φόρτισης. Αυτό είναι πειραματικό χαρακτηριστικό — παρακαλώ αναφέρετε αν συνεχίσει να αποτυγχάνει.</string>
<string name="device_settings_category_connections_label">Συνδεδεμένες Συσκευές</string>
<string name="device_settings_connected_devices_description">Άλλες συσκευές που είναι συνδεδεμένες στα AirPods αυτή τη στιγμή</string>
<string name="device_settings_connected_device_label">Συσκευή %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparencia de fondo</string>
<string name="widget_config_show_device_label">Mostrar nombre del dispositivo</string>
<string name="widget_config_reset_label">Restablecer valores predeterminados</string>
<string name="widget_config_preview_resize_hint">Podés redimensionar el widget después de colocarlo en tu pantalla de inicio.</string>
<string name="widget_config_custom_label">Personalizado</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Oscuro</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Es un dispositivo desconocido, pero está utilizando un formato similar. Añadamos soporte para ello, contáctame :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
<string name="pods_charging_label">Cargando</string>
<string name="pods_charging_optimized_label">Optimizado</string>
<string name="pods_inear_label">En el oído</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="anc_mode_off">Desactivado</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Nombre de dispositivo Bluetooth</string>
<string name="device_settings_info_model_label">Modelo</string>
<string name="device_settings_info_manufacturer_label">Fabricante</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Número de serie</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware pendiente</string>
<string name="device_settings_info_build_label">Compilación</string>
<string name="device_settings_info_left_serial_label">Número de serie del auricular izquierdo</string>
<string name="device_settings_info_right_serial_label">Número de serie del auricular derecho</string>
<string name="device_settings_info_left_bonded_label">Izquierdo vinculado</string>
<string name="device_settings_info_right_bonded_label">Derecho vinculado</string>
<string name="device_settings_info_details_label">Detalles del dispositivo</string>
<string name="device_settings_info_details_action">Mostrar detalles del dispositivo</string>
<string name="device_settings_info_status_label">Estado</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Este dispositivo está cerca pero no conectado a este teléfono. Conectate para acceder a la configuración y los controles.</string>
<string name="device_settings_not_connected_connect_action">Conectar</string>
<string name="device_settings_not_connected_open_settings_action">Abrir ajustes de Bluetooth</string>
<string name="device_settings_not_nearby_label">Dispositivo no está cerca</string>
<string name="device_settings_not_nearby_description">La configuración solo está disponible cuando este dispositivo está cerca y conectado a tu teléfono.</string>
<string name="device_settings_aap_unavailable_label">Configuración avanzada no disponible</string>
<string name="device_settings_aap_unavailable_description">La configuración avanzada de AirPods requiere una función de Bluetooth que no está disponible en este teléfono. Esto puede resolverse con una futura actualización de Android del fabricante de tu dispositivo.</string>
<string name="device_settings_aap_unavailable_action">Ver rastreador de compatibilidad</string>
<string name="device_settings_category_sound_label">Sonido</string>
<string name="device_settings_category_controls_label">Controles</string>
<string name="device_settings_nc_one_airpod_label">ANC con un solo auricular</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Pulsación simple para silenciar el micrófono</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Doble pulsación para finalizar la llamada</string>
<string name="device_settings_open_cd">Abrir configuración del dispositivo</string>
<string name="overview_card_missing_paired_device_cd">El perfil no tiene un dispositivo Bluetooth vinculado — tocá para editar el perfil</string>
<string name="overview_card_missing_paired_device">Este perfil no tiene ningún dispositivo Bluetooth emparejado.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">General</string>
<string name="device_settings_microphone_mode_label">Micrófono</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Permitió Desactivado como modo de control de ruido seleccionable. Cuando está deshabilitado, los tallos omiten Desactivado al ciclar.</string>
<string name="device_settings_sleep_detection_label">Detección de sueño</string>
<string name="device_settings_sleep_detection_description">Pausar automáticamente el audio cuando te quedas dormido</string>
<string name="reaction_sleep_channel_label">Detección de sueño</string>
<string name="reaction_sleep_notification_title">Pausado por Detección de sueño</string>
<string name="reaction_sleep_notification_text">%1$s informó que te quedaste dormido, por lo que tu música fue pausada. Podés desactivar esto en la configuración del dispositivo.</string>
<string name="device_settings_rename_label">Renombrar</string>
<string name="device_settings_rename_hint">Nombre del dispositivo</string>
<string name="device_settings_rename_confirm">Renombrar</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Ajustes de Bluetooth</string>
<string name="device_settings_send_failed">No se pudo aplicar la configuración: %1$s</string>
<string name="device_settings_anc_off_rejected_message">El modo Desactivado no está habilitado en este dispositivo. Activá \"Permitir modo Desactivado\" en Control de ruido.</string>
<string name="device_settings_category_battery_label">Batería</string>
<string name="device_settings_charge_cap_label">Límite de carga optimizado</string>
<string name="device_settings_charge_cap_description">Aprende tu rutina y pausa la carga alrededor del 80%% para prolongar la vida de la batería, terminando de cargar los auriculares antes de que sea probable que los uses.</string>
<string name="device_settings_charge_cap_rejected_message">No se pudo cambiar el Límite de carga optimizado. Esta es una función experimental — informá si sigue fallando.</string>
<string name="device_settings_category_connections_label">Dispositivos conectados</string>
<string name="device_settings_connected_devices_description">Otros dispositivos actualmente conectados a estos AirPods</string>
<string name="device_settings_connected_device_label">Dispositivo %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparencia de fondo</string>
<string name="widget_config_show_device_label">Mostrar nombre del dispositivo</string>
<string name="widget_config_reset_label">Restablecer predeterminados</string>
<string name="widget_config_preview_resize_hint">Puedes cambiar el tamaño del widget después de colocarlo en tu pantalla de inicio.</string>
<string name="widget_config_custom_label">Personalizado</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Oscuro</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Es un dispositivo desconocido, pero está utilizando un formato similar. Añadamos soporte para ello, contáctame :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
<string name="pods_charging_label">Cargando</string>
<string name="pods_charging_optimized_label">Optimizado</string>
<string name="pods_inear_label">En el oído</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="anc_mode_off">Apagado</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Etiqueta del dispositivo Bluetooth</string>
<string name="device_settings_info_model_label">Modelo</string>
<string name="device_settings_info_manufacturer_label">Fabricante</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Número de serie</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware pendiente</string>
<string name="device_settings_info_build_label">Compilación</string>
<string name="device_settings_info_left_serial_label">Número de serie del auricular izquierdo</string>
<string name="device_settings_info_right_serial_label">Número de serie del auricular derecho</string>
<string name="device_settings_info_left_bonded_label">Izquierdo vinculado</string>
<string name="device_settings_info_right_bonded_label">Derecho vinculado</string>
<string name="device_settings_info_details_label">Detalles del dispositivo</string>
<string name="device_settings_info_details_action">Mostrar detalles del dispositivo</string>
<string name="device_settings_info_status_label">Estado</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Este dispositivo está cerca pero no conectado a este teléfono. Conéctalo para acceder a la configuración y controles.</string>
<string name="device_settings_not_connected_connect_action">Conectar</string>
<string name="device_settings_not_connected_open_settings_action">Abrir ajustes de Bluetooth</string>
<string name="device_settings_not_nearby_label">Dispositivo no está cerca</string>
<string name="device_settings_not_nearby_description">La configuración solo está disponible cuando este dispositivo está cerca y conectado a tu teléfono.</string>
<string name="device_settings_aap_unavailable_label">Configuración avanzada no disponible</string>
<string name="device_settings_aap_unavailable_description">La configuración avanzada de AirPods requiere una función de Bluetooth que no está disponible en este teléfono. Esto podría resolverse con una futura actualización de Android de tu fabricante.</string>
<string name="device_settings_aap_unavailable_action">Ver rastreador de compatibilidad</string>
<string name="device_settings_category_sound_label">Sonido</string>
<string name="device_settings_category_controls_label">Controles</string>
<string name="device_settings_nc_one_airpod_label">ANC con un solo auricular</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Pulsación simple para silenciar el micrófono</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Pulsación doble para terminar la llamada</string>
<string name="device_settings_open_cd">Abrir ajustes del dispositivo</string>
<string name="overview_card_missing_paired_device_cd">El perfil no tiene un dispositivo Bluetooth vinculado — toca para editar el perfil</string>
<string name="overview_card_missing_paired_device">Este perfil no tiene ningún dispositivo Bluetooth vinculado.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">General</string>
<string name="device_settings_microphone_mode_label">Micrófono</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Permitir Apagado como modo de control de ruido seleccionable. Cuando está desactivado, los tallos omiten Apagado al ciclar.</string>
<string name="device_settings_sleep_detection_label">Detección de sueño</string>
<string name="device_settings_sleep_detection_description">Pausar automáticamente el audio cuando te quedas dormido</string>
<string name="reaction_sleep_channel_label">Detección de sueño</string>
<string name="reaction_sleep_notification_title">Pausado por detección de sueño</string>
<string name="reaction_sleep_notification_text">%1$s detectó que te quedaste dormido, por lo que tu música fue pausada. Puedes desactivar esto en la configuración del dispositivo.</string>
<string name="device_settings_rename_label">Renombrar</string>
<string name="device_settings_rename_hint">Nombre del dispositivo</string>
<string name="device_settings_rename_confirm">Renombrar</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Ajustes de Bluetooth</string>
<string name="device_settings_send_failed">No se pudo aplicar la configuración: %1$s</string>
<string name="device_settings_anc_off_rejected_message">El modo Apagado no está habilitado en este dispositivo. Activa \"Permitir modo Apagado\" en Control de ruido.</string>
<string name="device_settings_category_battery_label">Batería</string>
<string name="device_settings_charge_cap_label">Límite de carga optimizado</string>
<string name="device_settings_charge_cap_description">Aprende tu rutina y pausa la carga alrededor del 80%% para prolongar la vida útil de la batería, cargando los pods antes de que los uses.</string>
<string name="device_settings_charge_cap_rejected_message">No se pudo cambiar el límite de carga optimizado. Esta es una función experimental — por favor repórtalo si sigue fallando.</string>
<string name="device_settings_category_connections_label">Dispositivos conectados</string>
<string name="device_settings_connected_devices_description">Otros dispositivos actualmente conectados a estos AirPods</string>
<string name="device_settings_connected_device_label">Dispositivo %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparencia del fondo</string>
<string name="widget_config_show_device_label">Mostrar el nombre del dispositivo</string>
<string name="widget_config_reset_label">Restaurar los valores predeterminados</string>
<string name="widget_config_preview_resize_hint">Puedes cambiar el tamaño del widget después de colocarlo en la pantalla de inicio.</string>
<string name="widget_config_custom_label">Personalizado</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Oscuro</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Es un dispositivo desconocido, pero está utilizando un formato similar. Añadamos soporte para ello, contáctame :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
<string name="pods_charging_label">Cargando</string>
<string name="pods_charging_optimized_label">Optimizado</string>
<string name="pods_inear_label">En el oído</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="anc_mode_off">Desactivado</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Etiqueta del dispositivo Bluetooth</string>
<string name="device_settings_info_model_label">Modelo</string>
<string name="device_settings_info_manufacturer_label">Fabricante</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Número de serie</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware pendiente</string>
<string name="device_settings_info_build_label">Versión</string>
<string name="device_settings_info_left_serial_label">Número de serie del auricular izquierdo</string>
<string name="device_settings_info_right_serial_label">Número de serie del auricular derecho</string>
<string name="device_settings_info_left_bonded_label">Izquierdo vinculado</string>
<string name="device_settings_info_right_bonded_label">Derecho vinculado</string>
<string name="device_settings_info_details_label">Detalles del dispositivo</string>
<string name="device_settings_info_details_action">Mostrar detalles del dispositivo</string>
<string name="device_settings_info_status_label">Estado</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Este dispositivo está cerca pero no conectado a este teléfono. Conéctalo para acceder a los ajustes y controles.</string>
<string name="device_settings_not_connected_connect_action">Conectar</string>
<string name="device_settings_not_connected_open_settings_action">Abrir ajustes de Bluetooth</string>
<string name="device_settings_not_nearby_label">Dispositivo no cercano</string>
<string name="device_settings_not_nearby_description">La configuración solo está disponible cuando este dispositivo está cerca y conectado a tu teléfono.</string>
<string name="device_settings_aap_unavailable_label">Ajustes avanzados no disponibles</string>
<string name="device_settings_aap_unavailable_description">La configuración avanzada de AirPods requiere una función Bluetooth que no está disponible en este teléfono. Esto podría resolverse con una futura actualización de Android del fabricante de tu dispositivo.</string>
<string name="device_settings_aap_unavailable_action">Ver rastreador de compatibilidad</string>
<string name="device_settings_category_sound_label">Sonido</string>
<string name="device_settings_category_controls_label">Controles</string>
<string name="device_settings_nc_one_airpod_label">ANC con un solo auricular</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Pulsación simple para silenciar el micrófono</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Doble pulsación para finalizar llamada</string>
<string name="device_settings_open_cd">Abrir configuración del dispositivo</string>
<string name="overview_card_missing_paired_device_cd">El perfil no tiene ningún dispositivo Bluetooth emparejado — toca para editar el perfil</string>
<string name="overview_card_missing_paired_device">Este perfil no tiene ningún dispositivo Bluetooth emparejado.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">General</string>
<string name="device_settings_microphone_mode_label">Micrófono</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Permite el modo Desactivado como modo de control de ruido seleccionable. Cuando está deshabilitado, el tallo omite Desactivado al ciclar.</string>
<string name="device_settings_sleep_detection_label">Detección de sueño</string>
<string name="device_settings_sleep_detection_description">Pausar el audio automáticamente cuando te quedes dormido</string>
<string name="reaction_sleep_channel_label">Detección de sueño</string>
<string name="reaction_sleep_notification_title">Pausado por detección de sueño</string>
<string name="reaction_sleep_notification_text">%1$s informó que te quedaste dormido, por lo que tu música fue pausada. Puedes desactivar esto en la configuración del dispositivo.</string>
<string name="device_settings_rename_label">Renombrar</string>
<string name="device_settings_rename_hint">Nombre del dispositivo</string>
<string name="device_settings_rename_confirm">Renombrar</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Ajustes de Bluetooth</string>
<string name="device_settings_send_failed">No se pudo aplicar el ajuste: %1$s</string>
<string name="device_settings_anc_off_rejected_message">El modo Desactivado no está habilitado en este dispositivo. Activa \"Permitir modo Desactivado\" en Control de ruido.</string>
<string name="device_settings_category_battery_label">Batería</string>
<string name="device_settings_charge_cap_label">Límite de carga optimizado</string>
<string name="device_settings_charge_cap_description">Aprende tu rutina y pausa la carga alrededor del 80%% para alargar la vida de la batería, completando la carga de los auriculares antes de que vayas a usarlos.</string>
<string name="device_settings_charge_cap_rejected_message">No se pudo cambiar el límite de carga optimizado. Es una función experimental; informe si sigue fallando.</string>
<string name="device_settings_category_connections_label">Dispositivos conectados</string>
<string name="device_settings_connected_devices_description">Otros dispositivos conectados actualmente a estos AirPods</string>
<string name="device_settings_connected_device_label">Dispositivo %d</string>
+33 -17
View File
@@ -62,7 +62,7 @@
<string name="settings_signal_minimum_description">Väikseim signaali kvaliteet, mis seade peab omama, et seda peetaks teie omaks.</string>
<string name="settings_autoconnect_label">Automaatne ühendamine</string>
<string name="settings_autoconnect_description">Kui Android ei ühendu ise, saame seda taotleda. See lülitab jälgimisrežiimi sisse alatiseks.</string>
<string name="settings_autoconnect_info_android12">Automaatne ühendus tugineb süsteemifunktsioonile, mida Android 12 ja uuemad versioonid piiravad. See ei pruugi teie seadmel toimida.</string>
<string name="settings_autoconnect_info_android12">Iseühenduvus tugineb süsteemivõimalusele, mida Android 12 ja uuemad versioonid piiravad. See ei pruugi teie seadmel toimida.</string>
<string name="settings_autoconnect_condition_label">Automaatse ühendamise tingimus</string>
<string name="settings_autoconnect_condition_description">Millal võime üritada ühendada teie seadet?</string>
<string name="settings_devices_label">Seadmed</string>
@@ -76,7 +76,7 @@
<string name="settings_compat_offloaded_batching_disabled_title">Keela riistvara rühmitamine</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Keelake süsteemil rühmitada kogutud andmeid madalama energiatarbimisega Bluetoothi kohta enne meile saatmist.</string>
<string name="settings_onepod_mode_label">Ühe klapi režiim</string>
<string name="settings_onepod_mode_description">Automaatne esitus/paus ja automaatühendus reageerivad ühele kõrvaklapile, mitte ei nõua mõlemat.</string>
<string name="settings_onepod_mode_description">Automaatne esitus/paus ja iseühenduvus reageerivad ühele kõrvaklapile, mitte ei nõua mõlemat.</string>
<string name="settings_popup_caseopen_label">Näita kaane hüpikakent</string>
<string name="settings_popup_caseopen_description">Seadme kaane avamisel näita hüpikakent (katsetamisel).</string>
<string name="settings_popup_connected_label">Näita ühenduse hüpikakent</string>
@@ -84,8 +84,8 @@
<string name="settings_popup_info_not_in_app">Hüpikaknad kuvatakse ainult siis, kui sa pole rakenduses.</string>
<string name="settings_popup_warning_manual_mode">Hüpikaknad nõuavad monitori töötamist. Monitori režiim on seatud „Kui rakendus on avatud“, mis peatab monitori, kui rakendusest lahkud.</string>
<string name="general_fix_it_action">Paranda</string>
<string name="device_settings_experimental_title">Eksperimentaalne funktsioon</string>
<string name="device_settings_experimental_description">Seda funktsiooni ei ole kõigil seadmetel veel testitud. Kui midagi ei tööta korrektselt, avage probleem koos silumislogi failiga.</string>
<string name="device_settings_experimental_title">Katsetamisel</string>
<string name="device_settings_experimental_description">Seda pole kõigil seadmetel veel katsetatud. Kui midagi ei toimi õigesti, avage probleem koos silumislogi failiga.</string>
<string name="device_settings_experimental_action">Ava probleemide jälgija</string>
<string name="notification_channel_device_status_label">Seadme olek</string>
<string name="notification_channel_device_status_connected_label">Ühendatud seade</string>
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Tausta läbipaistvus</string>
<string name="widget_config_show_device_label">Kuva seadme nimi</string>
<string name="widget_config_reset_label">Lähtesta vaikeseadistused</string>
<string name="widget_config_preview_resize_hint">Pärast vidina asetamist avaekraanile saate muuta selle suurust.</string>
<string name="widget_config_custom_label">Kohandatud</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Tume</string>
@@ -190,9 +191,9 @@
<string name="general_manage_devices_action">Halda seadmeid</string>
<string name="overview_nomaindevice_label">Seadistatud seadmed puuduvad</string>
<string name="overview_nomaindevice_description">Aku taseme jälgimiseks ja lisavõimaluste lubamiseks seadistage oma seade.</string>
<string name="overview_bluetooth_disabled_icon_cd">Bluetooth on keelatud, ava seaded</string>
<string name="overview_bluetooth_nearby_icon_cd">Seade on lähedal, ava Bluetoothi seaded</string>
<string name="overview_bluetooth_connected_icon_cd">Seade on ühendatud, ava Bluetoothi seaded</string>
<string name="overview_bluetooth_disabled_icon_cd">Bluetooth on keelatud, avage seaded</string>
<string name="overview_bluetooth_nearby_icon_cd">Seade on lähedal, avage Bluetoothi seaded</string>
<string name="overview_bluetooth_connected_icon_cd">Seade on ühendatud, avage Bluetoothi seaded</string>
<string name="overview_bluetooth_disabled_label">Bluetooth on välja lülitatud</string>
<string name="overview_bluetooth_disabled_description">Bluetooth on keelatud, lubage see ;)</string>
<string name="overview_monitoring_active_label">Seadmete jälgimine</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">See on tundmatu seade, mis kasutab sarnast teatevormingut. Las ma muudan selle ühilduvaks, selleks võta minuga ühendust :)</string>
<string name="pods_none_label_short">Puudub seade</string>
<string name="pods_charging_label">Laeb</string>
<string name="pods_charging_optimized_label">Optimeeritud</string>
<string name="pods_inear_label">Kõrvas</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Väljas</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetoothi seadme silt</string>
<string name="device_settings_info_model_label">Mudel</string>
<string name="device_settings_info_manufacturer_label">Tootja</string>
<string name="device_settings_info_hardware_label">Riistvara</string>
<string name="device_settings_info_serial_label">Seerianumber</string>
<string name="device_settings_info_firmware_label">Püsivara</string>
<string name="device_settings_info_firmware_pending_label">Ootel püsivara</string>
<string name="device_settings_info_build_label">Versioon</string>
<string name="device_settings_info_left_serial_label">Vasaku kõrvaklapiosa seerianumber</string>
<string name="device_settings_info_right_serial_label">Parema kõrvaklapiosa seerianumber</string>
<string name="device_settings_info_left_bonded_label">Vasak seotud</string>
<string name="device_settings_info_right_bonded_label">Parem seotud</string>
<string name="device_settings_info_details_label">Seadme andmed</string>
<string name="device_settings_info_details_action">Kuva seadme andmed</string>
<string name="device_settings_info_status_label">Olek</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">See seade on lähedal, kuid pole selle telefoniga ühendatud. Seadete ja juhtelementide kasutamiseks ühendage seade.</string>
<string name="device_settings_not_connected_connect_action">Ühenda</string>
<string name="device_settings_not_connected_open_settings_action">Ava Bluetoothi seaded</string>
<string name="device_settings_not_nearby_label">Seade on kaugel</string>
<string name="device_settings_not_nearby_description">Seaded on saadaval ainult siis, kui see seade on lähedal ja teie telefoniga ühendatud.</string>
<string name="device_settings_aap_unavailable_label">Täpsemad seaded pole saadaval</string>
<string name="device_settings_aap_unavailable_description">Täpsemad AirPodsi seaded nõuavad Bluetoothi funktsiooni, mis pole sellel telefonis saadaval. See võib laheneda teie seadme tootja tulevase Androidi värskendusega.</string>
<string name="device_settings_aap_unavailable_description">Täpsemad AirPodsi seaded nõuavad Bluetoothi, mis pole selles telefonis saadaval. See võib laheneda teie seadme tootja tulevase Androidi värskendusega.</string>
<string name="device_settings_aap_unavailable_action">Kuva ühilduvuse jälgija</string>
<string name="device_settings_category_sound_label">Heli</string>
<string name="device_settings_category_controls_label">Juhtnupud</string>
<string name="device_settings_nc_one_airpod_label">ANC ühe kõrvaklapiosaga</string>
@@ -434,7 +443,7 @@
<string name="device_settings_tone_volume_description">Kinnitushelide ja helinate helitugevus</string>
<string name="device_settings_adaptive_noise_label">Kohanduva heli müra</string>
<string name="device_settings_adaptive_noise_description">Kui palju keskkonna müra läbi lastakse</string>
<string name="device_settings_adaptive_noise_requires_adaptive">Nõuab kohanduva mürakontrolli</string>
<string name="device_settings_adaptive_noise_requires_adaptive">Nõuab kohanduvat mürakontrolli</string>
<string name="device_settings_pending_info">Muudatused jõustuvad, kui AirPodsid on kõrvades</string>
<string name="device_settings_press_speed_label">Vajutuse kiirus</string>
<string name="device_settings_press_speed_description">Kui kiiresti pead vajutama mitme vajutuse žestide jaoks</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Ühe vajutusega vaigista mikrofon</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Kahe vajutusega lõpeta kõne</string>
<string name="device_settings_open_cd">Ava seadme seaded</string>
<string name="overview_card_missing_paired_device_cd">Profiilil pole seotud Bluetooth-seadet — profiili muutmiseks puuduta</string>
<string name="overview_card_missing_paired_device">Sellel profiilil puudub seotud Bluetoothi seade.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Üldine</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -474,15 +483,18 @@
<string name="device_settings_listening_mode_cycle_dialog_title">Kohandage pikalt vajutamise tsüklit</string>
<string name="device_settings_listening_mode_cycle_summary_helper">Kasutatakse varretikku hoides ja kiirvalijas</string>
<string name="device_settings_listening_mode_cycle_summary_helper_override">Kasutatakse kiirvalijas. Kohandatud varretiku pika vajutuse toiming asendab AirPodside tsüklimise.</string>
<string name="device_settings_listening_mode_cycle_minimum">Vähemalt 2 režiimi peab jääma lubatust.</string>
<string name="device_settings_listening_mode_cycle_minimum">Vähemalt 2 režiimi peab jääma lubatuks.</string>
<string name="device_settings_listening_mode_cycle_off">Väljas</string>
<string name="device_settings_listening_mode_cycle_anc">Mürasummutus</string>
<string name="device_settings_listening_mode_cycle_transparency">Läbipaistvus</string>
<string name="device_settings_listening_mode_cycle_adaptive">Kohanduv</string>
<string name="device_settings_allow_off_label">Luba väljalülitatuse režiim</string>
<string name="device_settings_allow_off_description">Luba Välja valikuna mürakontrolli režiimis. Kui keelatud, jätavad varretikud tsüklimise ajal Välja vahele.</string>
<string name="device_settings_allow_off_description">Luba Väljas“ valik mürakontrolli režiimis. Kui keelatud, jätavad varretikud tsüklimise ajal Väljas“ vahele.</string>
<string name="device_settings_sleep_detection_label">Une tuvastamine</string>
<string name="device_settings_sleep_detection_description">Heli vaigistub, kui jääd magama</string>
<string name="reaction_sleep_channel_label">Une tuvastamine</string>
<string name="reaction_sleep_notification_title">Uinumise tõttu peatatud</string>
<string name="reaction_sleep_notification_text">%1$s teatas, et uinusite, seega muusika vaikis. Saate selle seadme seadetes keelata.</string>
<string name="device_settings_rename_label">Nimeta ümber</string>
<string name="device_settings_rename_hint">Seadme nimi</string>
<string name="device_settings_rename_confirm">Nimeta ümber</string>
@@ -491,7 +503,11 @@
<string name="device_settings_rename_system_unavailable">Android ei lubanud meil seadet siin ümber nimetada. Bluetoothi seadetes nime uuendamiseks nimetage see seal ümber või siduge seade uuesti.</string>
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetoothi seaded</string>
<string name="device_settings_send_failed">Seadet ei saanud rakendada: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Väljas režiim ei ole sellel seadmel lubatud. Lubage \"Luba väljalülitatuse režiim\" mürakontrolli all.</string>
<string name="device_settings_anc_off_rejected_message">Väljas-režiim pole sellel seadmel lubatud. Lubage Luba väljalülitatuse režiim mürakontrolli all.</string>
<string name="device_settings_category_battery_label">Aku</string>
<string name="device_settings_charge_cap_label">Optimeeritud laadimispiirang</string>
<string name="device_settings_charge_cap_description">Õpib kasutamisharjumusest ja peatab laadimise umbes 80%% juures, et pikendada aku kasutusiga, laadides kuularid täis enne, kui neid tõenäoliselt kasutate.</string>
<string name="device_settings_charge_cap_rejected_message">Optimeeritud laadimispiirangut ei saanud muuta. Seda katsetatakse — teavitage, kui see ikka nurjub.</string>
<string name="device_settings_category_connections_label">Ühendatud seadmed</string>
<string name="device_settings_connected_devices_description">Muud seadmed, mis on hetkel nende AirPodidega ühendatud</string>
<string name="device_settings_connected_device_label">Seade %d</string>
@@ -502,7 +518,7 @@
<!-- Press Controls -->
<string name="press_controls_title">Vajutuse juhtelemendid</string>
<string name="press_controls_nav_description">Häälesta vajutuskäitumist ja mäaarake vajutustele toimingud</string>
<string name="press_controls_description">Häälesta, kuidas vajutused tuvastatakse, valige kõnenuppu käitumine ja määrake vajutustele toimingud. Määratud toimingud kehtivad ainult sellele seadmele ja lasevad rakendusel vajutuse vahele võtta, mitte AirPodsidel seda looduslikult käsitseda.</string>
<string name="press_controls_description">Häälestage, kuidas vajutused tuvastatakse, valige kõnenuppu käitumine ja määrake vajutustele toimingud. Määratud toimingud kehtivad ainult sellele seadmele ja lasevad rakendusel vajutust mõjutada, mitte AirPodsidel seda käsitseda.</string>
<string name="press_controls_single_press">Üks vajutus</string>
<string name="press_controls_double_press">Kaks vajutust</string>
<string name="press_controls_triple_press">Kolm vajutust</string>
@@ -513,18 +529,18 @@
<string name="press_controls_right">Parem</string>
<string name="press_action_none">Tavaline</string>
<string name="press_action_no_action">Toiming puudub</string>
<string name="press_action_play_pause">Esita/Peata</string>
<string name="press_action_play_pause">Esita/peata</string>
<string name="press_action_next_track">Järgmine lugu</string>
<string name="press_action_previous_track">Eelmine lugu</string>
<string name="press_action_volume_up">Valjemaks</string>
<string name="press_action_volume_down">Vaiksemaks</string>
<string name="press_action_mute_toggle">Vaigista muusika</string>
<string name="press_action_stop">Peata</string>
<string name="press_action_fast_forward">Kiirkelleramine</string>
<string name="press_action_fast_forward">Kiirkerimine</string>
<string name="press_action_rewind">Tagasikerimine</string>
<string name="press_action_cycle_anc">Mürakontrolli tsükkel</string>
<string name="press_action_toggle_anc_transparency">Vaheta läbipaistvust</string>
<string name="press_controls_reset_label">Taasta tavaseadistused</string>
<string name="press_controls_reset_confirm_message">Lähestada kõik vajutuse vastendused vaikeväärtustele?</string>
<string name="press_controls_reset_confirm_message">Lähestada kõik vajutuse vastendused tavaväärtustele?</string>
<string name="device_settings_noise_control_open_press_controls_action">Ava vajutuse juhtelemendid</string>
</resources>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Atzeko planoaren gardentasuna</string>
<string name="widget_config_show_device_label">Gailuaren izena erakutsi</string>
<string name="widget_config_reset_label">Berriro lehenespenera berrezarri</string>
<string name="widget_config_preview_resize_hint">Widgeta tamainaz alda dezakezu hasierako pantailan jarri ondoren.</string>
<string name="widget_config_custom_label">Pertsonalizatua</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Iluna</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Gailu ezezagun bat da, baina mezu-formatu berdina erabiltzen du. Gehitu dezagun euskarria, jarri nirekin harremanetan :)</string>
<string name="pods_none_label_short">Gailurik ez</string>
<string name="pods_charging_label">Kargatzen</string>
<string name="pods_charging_optimized_label">Optimizatua</string>
<string name="pods_inear_label">Belarrian</string>
<string name="pods_microphone_label">Mikrofonoa</string>
<string name="anc_mode_off">Itzalita</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth gailuaren etiketa</string>
<string name="device_settings_info_model_label">Modeloa</string>
<string name="device_settings_info_manufacturer_label">Fabrikatzailea</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Serie-zenbakia</string>
<string name="device_settings_info_firmware_label">Firmware-a</string>
<string name="device_settings_info_firmware_pending_label">Firmware Zain</string>
<string name="device_settings_info_build_label">Eraikuntza</string>
<string name="device_settings_info_left_serial_label">Ezkerreko pod-aren seriea</string>
<string name="device_settings_info_right_serial_label">Eskuineko pod-aren seriea</string>
<string name="device_settings_info_left_bonded_label">Ezkerra Lotuta</string>
<string name="device_settings_info_right_bonded_label">Eskuina Lotuta</string>
<string name="device_settings_info_details_label">Gailuaren xehetasunak</string>
<string name="device_settings_info_details_action">Erakutsi gailuaren xehetasunak</string>
<string name="device_settings_info_status_label">Egoera</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Gailu hau gertu dago baina ez dago telefono honetara konektatuta. Konektatu ezarpenak eta kontrolak atzitzeko.</string>
<string name="device_settings_not_connected_connect_action">Konektatu</string>
<string name="device_settings_not_connected_open_settings_action">Ireki Bluetooth Ezarpenak</string>
<string name="device_settings_not_nearby_label">Gailua ez dago inguruan</string>
<string name="device_settings_not_nearby_description">Ezarpenak eskuragarri daude soilik gailu hau inguruan dagoenean eta zure telefonora konektatuta dagoenean.</string>
<string name="device_settings_aap_unavailable_label">Ezarpen aurreratuak ez daude erabilgarri</string>
<string name="device_settings_aap_unavailable_description">AirPods ezarpen aurreratuek telefono honetan erabilgarri ez dagoen Bluetooth funtzio bat behar dute. Baliteke etorkizuneko Android eguneraketa batek konpontzea, zure gailuaren fabrikatzailearen eskutik.</string>
<string name="device_settings_aap_unavailable_action">Ikusi bateragarritasun jarraipena</string>
<string name="device_settings_category_sound_label">Soinua</string>
<string name="device_settings_category_controls_label">Kontrolak</string>
<string name="device_settings_nc_one_airpod_label">ANC pod batekin</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Sakapen bakar bat mikrofonoa mututzeko</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Bikoitz sakatu deia amaitzeko</string>
<string name="device_settings_open_cd">Ireki gailuaren ezarpenak</string>
<string name="overview_card_missing_paired_device_cd">Profilek ez du parkatutako Bluetooth gailurik — sakatu profila editatzeko</string>
<string name="overview_card_missing_paired_device">Profil honek ez du Bluetooth gailurik parekaturik.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Orokorra</string>
<string name="device_settings_microphone_mode_label">Mikrofonoa</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Onartu Itzali zarata-kontrolaren modu aukeragarri gisa. Desgaitutakoan, zuztarrak Itzali saltatzen du zikloaren bitartean.</string>
<string name="device_settings_sleep_detection_label">Lo-detekzioa</string>
<string name="device_settings_sleep_detection_description">Pausatu audioa automatikoki lo geratzen zarenean</string>
<string name="reaction_sleep_channel_label">Lo Detekzioa</string>
<string name="reaction_sleep_notification_title">Lo Detekzioak Pausatuta</string>
<string name="reaction_sleep_notification_text">%1$s-ek jakinarazi du lo geratu zarela, eta musikak pausatu egin zen. Gailu-ezarpenetan desgaitu dezakezu.</string>
<string name="device_settings_rename_label">Berrizendatu</string>
<string name="device_settings_rename_hint">Gailuaren izena</string>
<string name="device_settings_rename_confirm">Berrizendatu</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth ezarpenak</string>
<string name="device_settings_send_failed">Ezin izan da ezarpena aplikatu: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Itzali modua ez dago aktibatuta gailu honetan. Gaitu \"Baimendu Itzali modua\" Zarata Kontrolen atalean.</string>
<string name="device_settings_category_battery_label">Bateria</string>
<string name="device_settings_charge_cap_label">Karga Muga Optimizatua</string>
<string name="device_settings_charge_cap_description">Zure errutina ikasi eta karga %% 80 inguruan pausatu bateriaren bizitza luzatzeko, podak erabiltzeko prest utziz.</string>
<string name="device_settings_charge_cap_rejected_message">Karga Muga Optimizatua ezin izan da aldatu. Funtzio esperimentala da — jakinarazi huts egiten jarraitzen badu.</string>
<string name="device_settings_category_connections_label">Konektatutako gailuak</string>
<string name="device_settings_connected_devices_description">AirPod hauetara konektatuta dauden beste gailuak</string>
<string name="device_settings_connected_device_label">%d. gailua</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">شفافیت پس‌زمینه</string>
<string name="widget_config_show_device_label">نمایش نام دستگاه</string>
<string name="widget_config_reset_label">بازنشانی به مقادیر پیش‌فرض</string>
<string name="widget_config_preview_resize_hint">می‌توانید پس از قرار دادن ویجت روی صفحه اصلی، اندازه آن را تغییر دهید.</string>
<string name="widget_config_custom_label">سفارشی</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">تیره</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">این یک دستگاه ناشناخته است، اما از فرمت پیام مشابهی استفاده می‌کند. بیایید پشتیبانی از آن را اضافه کنیم، با من تماس بگیرید :)</string>
<string name="pods_none_label_short">بدون دستگاه</string>
<string name="pods_charging_label">در حال شارژ</string>
<string name="pods_charging_optimized_label">بهینه‌شده</string>
<string name="pods_inear_label">در گوش</string>
<string name="pods_microphone_label">میکروفون</string>
<string name="anc_mode_off">خاموش</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">برچسب دستگاه بلوتوث</string>
<string name="device_settings_info_model_label">مدل</string>
<string name="device_settings_info_manufacturer_label">سازنده</string>
<string name="device_settings_info_hardware_label">سخت‌افزار</string>
<string name="device_settings_info_serial_label">شماره سریال</string>
<string name="device_settings_info_firmware_label">سیستم‌افزار</string>
<string name="device_settings_info_firmware_pending_label">فریمور در انتظار</string>
<string name="device_settings_info_build_label">بیلد</string>
<string name="device_settings_info_left_serial_label">شماره سریال پاد چپ</string>
<string name="device_settings_info_right_serial_label">شماره سریال پاد راست</string>
<string name="device_settings_info_left_bonded_label">چپ متصل</string>
<string name="device_settings_info_right_bonded_label">راست متصل</string>
<string name="device_settings_info_details_label">جزئیات دستگاه</string>
<string name="device_settings_info_details_action">نمایش جزئیات دستگاه</string>
<string name="device_settings_info_status_label">وضعیت</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">این دستگاه در نزدیکی است اما به این گوشی متصل نیست. برای دسترسی به تنظیمات و کنترل‌ها، متصل شوید.</string>
<string name="device_settings_not_connected_connect_action">اتصال</string>
<string name="device_settings_not_connected_open_settings_action">باز کردن تنظیمات بلوتوث</string>
<string name="device_settings_not_nearby_label">دستگاه در نزدیکی نیست</string>
<string name="device_settings_not_nearby_description">تنظیمات فقط زمانی در دسترس هستند که این دستگاه در نزدیکی باشد و به تلفن شما متصل باشد.</string>
<string name="device_settings_aap_unavailable_label">تنظیمات پیشرفته در دسترس نیست</string>
<string name="device_settings_aap_unavailable_description">تنظیمات پیشرفته AirPods به یک ویژگی بلوتوثی نیاز دارند که در این تلفن در دسترس نیست. ممکن است با یک بروزرسانی آینده اندروید از سازنده دستگاه شما این مشکل حل شود.</string>
<string name="device_settings_aap_unavailable_action">مشاهده ردیاب سازگاری</string>
<string name="device_settings_category_sound_label">صدا</string>
<string name="device_settings_category_controls_label">کنترل‌ها</string>
<string name="device_settings_nc_one_airpod_label">ANC با یک پاد</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">یک‌بار فشار برای بی‌صدا کردن میکروفون</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">دوبار فشار برای پایان دادن تماس</string>
<string name="device_settings_open_cd">باز کردن تنظیمات دستگاه</string>
<string name="overview_card_missing_paired_device_cd">پروفایل دستگاه بلوتوث جفت‌شده‌ای ندارد — برای ویرایش پروفایل ضربه بزنید</string>
<string name="overview_card_missing_paired_device">این پروفایل هیچ دستگاه بلوتوثی جفت‌شده‌ای ندارد.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">عمومی</string>
<string name="device_settings_microphone_mode_label">میکروفون</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">اجازه دادن به حالت خاموش به عنوان یک حالت قابل انتخاب کنترل نویز. وقتی غیرفعال است، ساقه‌ها هنگام چرخش از حالت خاموش رد می‌شوند.</string>
<string name="device_settings_sleep_detection_label">تشخیص خواب</string>
<string name="device_settings_sleep_detection_description">به‌طور خودکار صدا را مکث کن هنگامی که به خواب می‌روید</string>
<string name="reaction_sleep_channel_label">تشخیص خواب</string>
<string name="reaction_sleep_notification_title">توسط تشخیص خواب متوقف شد</string>
<string name="reaction_sleep_notification_text">%1$s گزارش داد که به خواب رفتید، بنابراین موسیقی شما متوقف شد. می‌توانید این را در تنظیمات دستگاه غیرفعال کنید.</string>
<string name="device_settings_rename_label">تغییر نام</string>
<string name="device_settings_rename_hint">نام دستگاه</string>
<string name="device_settings_rename_confirm">تغییر نام</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">تنظیمات بلوتوث</string>
<string name="device_settings_send_failed">نمی‌توان تنظیمات را اعمال کرد: %1$s</string>
<string name="device_settings_anc_off_rejected_message">حالت خاموش روی این دستگاه فعال نیست. «اجازه دادن به حالت خاموش» را در کنترل نویز فعال کنید.</string>
<string name="device_settings_category_battery_label">باتری</string>
<string name="device_settings_charge_cap_label">محدودیت شارژ بهینه</string>
<string name="device_settings_charge_cap_description">روتین شما را یاد می‌گیرد و شارژ را در حدود ۸۰%% متوقف می‌کند تا عمر باتری افزایش یابد و پادها را قبل از زمانی که احتمالاً از آن‌ها استفاده می‌کنید شارژ کامل می‌کند.</string>
<string name="device_settings_charge_cap_rejected_message">محدودیت شارژ بهینه قابل تغییر نبود. این یک ویژگی آزمایشی است — لطفاً در صورت ادامه خرابی گزارش دهید.</string>
<string name="device_settings_category_connections_label">دستگاه‌های متصل</string>
<string name="device_settings_connected_devices_description">دستگاه‌های دیگری که در حال حاضر به این AirPods متصل هستند</string>
<string name="device_settings_connected_device_label">دستگاه %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Taustan läpinäkyvyys</string>
<string name="widget_config_show_device_label">Näytä laitteen nimi</string>
<string name="widget_config_reset_label">Palauta oletusasetukset</string>
<string name="widget_config_preview_resize_hint">Voit muuttaa widgetin kokoa sen asettamisen jälkeen aloitusnäytöllä.</string>
<string name="widget_config_custom_label">Mukautettu</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Tumma</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Tämä on tuntematon laite, mutta se käyttää samanlaista viestimuotoa. Lisätään sille tuki, ota yhteyttä :)</string>
<string name="pods_none_label_short">Ei laitetta</string>
<string name="pods_charging_label">Latautuu</string>
<string name="pods_charging_optimized_label">Optimoitu</string>
<string name="pods_inear_label">Korvassa</string>
<string name="pods_microphone_label">Mikrofoni</string>
<string name="anc_mode_off">Pois</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth-laitteen nimi</string>
<string name="device_settings_info_model_label">Malli</string>
<string name="device_settings_info_manufacturer_label">Valmistaja</string>
<string name="device_settings_info_hardware_label">Laitteisto</string>
<string name="device_settings_info_serial_label">Sarjanumero</string>
<string name="device_settings_info_firmware_label">Laiteohjelmisto</string>
<string name="device_settings_info_firmware_pending_label">Odottava laiteohjelmisto</string>
<string name="device_settings_info_build_label">Versio</string>
<string name="device_settings_info_left_serial_label">Vasemman kuulokkeen sarjanumero</string>
<string name="device_settings_info_right_serial_label">Oikean kuulokkeen sarjanumero</string>
<string name="device_settings_info_left_bonded_label">Vasen yhdistetty</string>
<string name="device_settings_info_right_bonded_label">Oikea yhdistetty</string>
<string name="device_settings_info_details_label">Laitteen tiedot</string>
<string name="device_settings_info_details_action">Näytä laitteen tiedot</string>
<string name="device_settings_info_status_label">Tila</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Tämä laite on lähellä, mutta ei yhdistettynä tähän puhelimeen. Yhdistä päästäksesi asetuksiin ja säätimiin.</string>
<string name="device_settings_not_connected_connect_action">Yhdistä</string>
<string name="device_settings_not_connected_open_settings_action">Avaa Bluetooth-asetukset</string>
<string name="device_settings_not_nearby_label">Laite ei ole lähellä</string>
<string name="device_settings_not_nearby_description">Asetukset ovat käytettävissä vain, kun tämä laite on lähellä ja yhdistettynä puhelimeesi.</string>
<string name="device_settings_aap_unavailable_label">Lisäasetukset eivät ole käytettävissä</string>
<string name="device_settings_aap_unavailable_description">AirPods-lisäasetukset vaativat Bluetooth-ominaisuuden, joka ei ole saatavilla tässä puhelimessa. Laitteen valmistajan tuleva Android-päivitys saattaa ratkaista tämän.</string>
<string name="device_settings_aap_unavailable_action">Katso yhteensopivuusseuranta</string>
<string name="device_settings_category_sound_label">Ääni</string>
<string name="device_settings_category_controls_label">Säätimet</string>
<string name="device_settings_nc_one_airpod_label">ANC yhdellä kuulokkeella</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Yksinkertainen painallus mykistää mikrofonin</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Kaksoispainallus lopettaa puhelun</string>
<string name="device_settings_open_cd">Avaa laiteasetukset</string>
<string name="overview_card_missing_paired_device_cd">Profiililla ei ole liitettyä Bluetooth-laitetta napauta muokataksesi profiilia</string>
<string name="overview_card_missing_paired_device">Tällä profiililla ei ole yhdistettyä Bluetooth-laitetta.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Yleiset</string>
<string name="device_settings_microphone_mode_label">Mikrofoni</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Salli Pois valittavaksi melunhallintatilaksi. Kun poistettu käytöstä, varret ohittavat Pois-tilan kierrossa.</string>
<string name="device_settings_sleep_detection_label">Unitunnistus</string>
<string name="device_settings_sleep_detection_description">Pysäytä ääni automaattisesti, kun nukahdat</string>
<string name="reaction_sleep_channel_label">Unitunnistus</string>
<string name="reaction_sleep_notification_title">Unitunnistus keskeytti toiston</string>
<string name="reaction_sleep_notification_text">%1$s havaitsi sinun nukahtaneen, joten musiikkisi keskeytettiin. Voit poistaa tämän käytöstä laiteasetuksista.</string>
<string name="device_settings_rename_label">Nimeä uudelleen</string>
<string name="device_settings_rename_hint">Laitteen nimi</string>
<string name="device_settings_rename_confirm">Nimeä uudelleen</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth-asetukset</string>
<string name="device_settings_send_failed">Asetusta ei voitu ottaa käyttöön: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Pois-tila ei ole käytössä tässä laitteessa. Ota käyttöön \"Salli Pois-tila\" Melunhallinnassa.</string>
<string name="device_settings_category_battery_label">Akku</string>
<string name="device_settings_charge_cap_label">Optimoitu latausraja</string>
<string name="device_settings_charge_cap_description">Oppii rutiinisi ja keskeyttää lataamisen noin 80 %%:iin akkukeston pidentämiseksi, täydentäen kuulokkeet ennen todennäköistä käyttöä.</string>
<string name="device_settings_charge_cap_rejected_message">Optimoitua latausrajaa ei voitu muuttaa. Tämä on kokeellinen ominaisuus ilmoita asiasta, jos ongelma toistuu.</string>
<string name="device_settings_category_connections_label">Yhdistetyt laitteet</string>
<string name="device_settings_connected_devices_description">Muut laitteet, jotka ovat tällä hetkellä yhdistetty näihin AirPodeihin</string>
<string name="device_settings_connected_device_label">Laite %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparency ng background</string>
<string name="widget_config_show_device_label">Ipakita ang pangalan ng device</string>
<string name="widget_config_reset_label">I-reset sa mga default</string>
<string name="widget_config_preview_resize_hint">Maaari mong baguhin ang laki ng widget pagkatapos ilagay ito sa iyong home screen.</string>
<string name="widget_config_custom_label">Pasadyang</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Madilim</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Ito ay isang hindi kilalang device, ngunit gumagamit ito ng katulad na format ng mensahe. Magdagdag tayo ng suporta para dito, kontakin mo ako :)</string>
<string name="pods_none_label_short">Walang device</string>
<string name="pods_charging_label">Nagcha-charge</string>
<string name="pods_charging_optimized_label">Na-optimize</string>
<string name="pods_inear_label">Nasa tainga</string>
<string name="pods_microphone_label">Mikropono</string>
<string name="anc_mode_off">Naka-off</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Label ng Bluetooth Device</string>
<string name="device_settings_info_model_label">Modelo</string>
<string name="device_settings_info_manufacturer_label">Tagagawa</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Serial Number</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Nakabinbing Firmware</string>
<string name="device_settings_info_build_label">Build</string>
<string name="device_settings_info_left_serial_label">Serial ng Kaliwang Pod</string>
<string name="device_settings_info_right_serial_label">Serial ng Kanang Pod</string>
<string name="device_settings_info_left_bonded_label">Kaliwang Naka-bond</string>
<string name="device_settings_info_right_bonded_label">Kanang Naka-bond</string>
<string name="device_settings_info_details_label">Mga Detalye ng Device</string>
<string name="device_settings_info_details_action">Ipakita ang mga detalye ng device</string>
<string name="device_settings_info_status_label">Katayuan</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Ang device na ito ay malapit ngunit hindi nakakonekta sa teleponong ito. Kumonekta para ma-access ang mga setting at kontrol.</string>
<string name="device_settings_not_connected_connect_action">Kumonekta</string>
<string name="device_settings_not_connected_open_settings_action">Buksan ang Mga Setting ng Bluetooth</string>
<string name="device_settings_not_nearby_label">Hindi malapit ang device</string>
<string name="device_settings_not_nearby_description">Ang mga setting ay available lamang kapag ang device na ito ay malapit at nakakonekta sa iyong telepono.</string>
<string name="device_settings_aap_unavailable_label">Hindi available ang mga advanced na setting</string>
<string name="device_settings_aap_unavailable_description">Ang mga advanced na setting ng AirPods ay nangangailangan ng Bluetooth na tampok na hindi available sa teleponong ito. Maaaring malutas ito ng isang hinaharap na Android update mula sa tagagawa ng iyong device.</string>
<string name="device_settings_aap_unavailable_action">Tingnan ang compatibility tracker</string>
<string name="device_settings_category_sound_label">Tunog</string>
<string name="device_settings_category_controls_label">Mga Kontrol</string>
<string name="device_settings_nc_one_airpod_label">ANC na may isang pod</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Isang pindutin para i-mute ang mikropono</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Dalawang pindutin para tapusin ang tawag</string>
<string name="device_settings_open_cd">Buksan ang mga setting ng device</string>
<string name="overview_card_missing_paired_device_cd">Walang naka-pair na Bluetooth device ang profile — i-tap para i-edit ang profile</string>
<string name="overview_card_missing_paired_device">Ang profile na ito ay walang naipares na Bluetooth device.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Pangkalahatan</string>
<string name="device_settings_microphone_mode_label">Mikropono</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Payagan ang Off bilang mapipiling noise control mode. Kapag naka-disable, nilalaktawan ng mga stem ang Off habang nagcy-cycle.</string>
<string name="device_settings_sleep_detection_label">Sleep Detection</string>
<string name="device_settings_sleep_detection_description">Awtomatikong i-pause ang audio kapag nakatulog ka</string>
<string name="reaction_sleep_channel_label">Pagtuklas ng Pagtulog</string>
<string name="reaction_sleep_notification_title">Naka-pause ng Pagtuklas ng Pagtulog</string>
<string name="reaction_sleep_notification_text">Naiulat ng %1$s na nakatulog ka, kaya na-pause ang iyong musika. Maaari mong i-disable ito sa mga setting ng device.</string>
<string name="device_settings_rename_label">Palitan ang Pangalan</string>
<string name="device_settings_rename_hint">Pangalan ng device</string>
<string name="device_settings_rename_confirm">Palitan ang Pangalan</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Mga Setting ng Bluetooth</string>
<string name="device_settings_send_failed">Hindi ma-apply ang setting: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Hindi naka-enable ang Off mode sa device na ito. I-enable ang \"Payagan ang Off mode\" sa ilalim ng Noise Control.</string>
<string name="device_settings_category_battery_label">Baterya</string>
<string name="device_settings_charge_cap_label">Na-optimize na Limitasyon ng Pagsisingilin</string>
<string name="device_settings_charge_cap_description">Matuto ng iyong gawain at i-pause ang pagsisingilin sa paligid ng 80%% upang mapahaba ang buhay ng baterya, sinisisingilin ang mga pods bago mo pa malamang gamitin ang mga ito.</string>
<string name="device_settings_charge_cap_rejected_message">Hindi mabago ang Na-optimize na Limitasyon ng Pagsisingilin. Ito ay isang eksperimental na feature — mangyaring iulat kung patuloy itong nabibigo.</string>
<string name="device_settings_category_connections_label">Mga Nakakonektang Device</string>
<string name="device_settings_connected_devices_description">Iba pang mga device na kasalukuyang nakakonekta sa mga AirPods na ito</string>
<string name="device_settings_connected_device_label">Device %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparence de l\'arrière-plan</string>
<string name="widget_config_show_device_label">Afficher le nom de lappareil</string>
<string name="widget_config_reset_label">Rétablir les valeurs par défaut</string>
<string name="widget_config_preview_resize_hint">Vous pouvez redimensionner le widget après l\'avoir placé sur votre écran d\'accueil.</string>
<string name="widget_config_custom_label">Personnalisée</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Sombre</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Cet appareil est inconnu, mais il utilise un format de message semblable. Prenons-le en charge, contactez-moi :)</string>
<string name="pods_none_label_short">Aucun appareil</string>
<string name="pods_charging_label">En charge</string>
<string name="pods_charging_optimized_label">Optimisé</string>
<string name="pods_inear_label">Porté</string>
<string name="pods_microphone_label">Microphone</string>
<string name="anc_mode_off">Désactivé</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Étiquette de lappareil Bluetooth</string>
<string name="device_settings_info_model_label">Modèle</string>
<string name="device_settings_info_manufacturer_label">Fabricant</string>
<string name="device_settings_info_hardware_label">Matériel</string>
<string name="device_settings_info_serial_label">Numéro de série</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware en attente</string>
<string name="device_settings_info_build_label">Version</string>
<string name="device_settings_info_left_serial_label">Numéro de série de l’écouteur gauche</string>
<string name="device_settings_info_right_serial_label">Numéro de série de l’écouteur droit</string>
<string name="device_settings_info_left_bonded_label">Gauche appairé</string>
<string name="device_settings_info_right_bonded_label">Droite appairée</string>
<string name="device_settings_info_details_label">Détails de lappareil</string>
<string name="device_settings_info_details_action">Afficher les détails de lappareil</string>
<string name="device_settings_info_status_label">État</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Cet appareil est à proximité mais n\'est pas connecté à ce téléphone. Connectez-vous pour accéder aux paramètres et aux commandes.</string>
<string name="device_settings_not_connected_connect_action">Connecter</string>
<string name="device_settings_not_connected_open_settings_action">Ouvrir les paramètres Bluetooth</string>
<string name="device_settings_not_nearby_label">Appareil non à proximité</string>
<string name="device_settings_not_nearby_description">Les paramètres ne sont disponibles que lorsque cet appareil est à proximité et connecté à votre téléphone.</string>
<string name="device_settings_aap_unavailable_label">Paramètres avancés indisponibles</string>
<string name="device_settings_aap_unavailable_description">Les paramètres avancés des AirPods nécessitent une fonctionnalité Bluetooth qui nest pas disponible sur ce téléphone. Cela pourrait être résolu par une future mise à jour Android de votre fabricant dappareil.</string>
<string name="device_settings_aap_unavailable_action">Voir le suivi de compatibilité</string>
<string name="device_settings_category_sound_label">Son</string>
<string name="device_settings_category_controls_label">Contrôles</string>
<string name="device_settings_nc_one_airpod_label">ANC avec un seul écouteur</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Appui simple pour couper le microphone</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Double appui pour terminer lappel</string>
<string name="device_settings_open_cd">Ouvrir les paramètres de lappareil</string>
<string name="overview_card_missing_paired_device_cd">Le profil n\'a pas d\'appareil Bluetooth associé — appuyez pour modifier le profil</string>
<string name="overview_card_missing_paired_device">Ce profil n\'a aucun appareil Bluetooth associé.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Général</string>
<string name="device_settings_microphone_mode_label">Microphone</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Autoriser Désactivé comme mode de contrôle du bruit sélectionnable. Lorsque désactivé, les tiges ignorent Désactivé lors du cycle.</string>
<string name="device_settings_sleep_detection_label">Détection du sommeil</string>
<string name="device_settings_sleep_detection_description">Mettre automatiquement en pause laudio quand vous vous endormez</string>
<string name="reaction_sleep_channel_label">Détection du sommeil</string>
<string name="reaction_sleep_notification_title">Mis en pause par la détection du sommeil</string>
<string name="reaction_sleep_notification_text">%1$s a détecté que vous vous êtes endormi, votre musique a donc été mise en pause. Vous pouvez désactiver cela dans les paramètres de l\'appareil.</string>
<string name="device_settings_rename_label">Renommer</string>
<string name="device_settings_rename_hint">Nom de lappareil</string>
<string name="device_settings_rename_confirm">Renommer</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Paramètres Bluetooth</string>
<string name="device_settings_send_failed">Impossible d\'appliquer le paramètre : %1$s</string>
<string name="device_settings_anc_off_rejected_message">Le mode Désactivé nest pas activé sur cet appareil. Activez « Autoriser le mode Désactivé » sous Contrôle du bruit.</string>
<string name="device_settings_category_battery_label">Batterie</string>
<string name="device_settings_charge_cap_label">Limite de charge optimisée</string>
<string name="device_settings_charge_cap_description">Apprend votre routine et met la charge en pause autour de 80 %% pour prolonger la durée de vie de la batterie, en rechargeant les écouteurs avant que vous soyez susceptible de les utiliser.</string>
<string name="device_settings_charge_cap_rejected_message">La limite de charge optimisée na pas pu être modifiée. Il sagit dune fonctionnalité expérimentale — veuillez signaler le problème si cela continue à échouer.</string>
<string name="device_settings_category_connections_label">Appareils connectés</string>
<string name="device_settings_connected_devices_description">Autres appareils actuellement connectés à ces AirPods</string>
<string name="device_settings_connected_device_label">Appareil %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparencia de fondo</string>
<string name="widget_config_show_device_label">Mostrar nome do dispositivo</string>
<string name="widget_config_reset_label">Restablecer aos valores predeterminados</string>
<string name="widget_config_preview_resize_hint">Podes cambiar o tamaño do widget despois de colocalo na pantalla de inicio.</string>
<string name="widget_config_custom_label">Personalizado</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Escuro</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Este é un dispositivo descoñecido, pero está a usar un formato de mensaxe similar. Engadamos compatibilidade para el, contacta comigo :)</string>
<string name="pods_none_label_short">Sen dispositivo</string>
<string name="pods_charging_label">Cargando</string>
<string name="pods_charging_optimized_label">Optimizado</string>
<string name="pods_inear_label">No oído</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="anc_mode_off">Desactivado</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Etiqueta do dispositivo Bluetooth</string>
<string name="device_settings_info_model_label">Modelo</string>
<string name="device_settings_info_manufacturer_label">Fabricante</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Número de serie</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware pendente</string>
<string name="device_settings_info_build_label">Compilación</string>
<string name="device_settings_info_left_serial_label">Número de serie do auricular esquerdo</string>
<string name="device_settings_info_right_serial_label">Número de serie do auricular dereito</string>
<string name="device_settings_info_left_bonded_label">Esquerdo vinculado</string>
<string name="device_settings_info_right_bonded_label">Dereito vinculado</string>
<string name="device_settings_info_details_label">Detalles do dispositivo</string>
<string name="device_settings_info_details_action">Mostrar detalles do dispositivo</string>
<string name="device_settings_info_status_label">Estado</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Este dispositivo está preto pero non está conectado a este teléfono. Conéctao para acceder á configuración e aos controis.</string>
<string name="device_settings_not_connected_connect_action">Conectar</string>
<string name="device_settings_not_connected_open_settings_action">Abrir a configuración de Bluetooth</string>
<string name="device_settings_not_nearby_label">Dispositivo non preto</string>
<string name="device_settings_not_nearby_description">A configuración só está dispoñible cando este dispositivo está preto e conectado ao teu teléfono.</string>
<string name="device_settings_aap_unavailable_label">Configuración avanzada non dispoñible</string>
<string name="device_settings_aap_unavailable_description">A configuración avanzada dos AirPods require unha función de Bluetooth que non está dispoñible neste teléfono. Isto pode resolverse cunha futura actualización de Android do fabricante do teu dispositivo.</string>
<string name="device_settings_aap_unavailable_action">Ver rastrexador de compatibilidade</string>
<string name="device_settings_category_sound_label">Son</string>
<string name="device_settings_category_controls_label">Controis</string>
<string name="device_settings_nc_one_airpod_label">ANC cun só auricular</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Prema unha vez para silenciar o micrófono</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Prema dúas veces para finalizar a chamada</string>
<string name="device_settings_open_cd">Abrir a configuración do dispositivo</string>
<string name="overview_card_missing_paired_device_cd">O perfil non ten ningún dispositivo Bluetooth emparellado: toca para editar o perfil</string>
<string name="overview_card_missing_paired_device">Este perfil non ten ningún dispositivo Bluetooth emparellado.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Xeral</string>
<string name="device_settings_microphone_mode_label">Micrófono</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Permite o modo Desactivado como modo de control de ruído seleccionable. Cando está desactivado, os tallos omiten Desactivado durante o ciclo.</string>
<string name="device_settings_sleep_detection_label">Detección do sono</string>
<string name="device_settings_sleep_detection_description">Pausar o audio automaticamente cando te quedas durmído</string>
<string name="reaction_sleep_channel_label">Detección de sono</string>
<string name="reaction_sleep_notification_title">Pausado pola detección de sono</string>
<string name="reaction_sleep_notification_text">%1$s detectou que te durmiches, polo que a túa música foi pausada. Podes desactivar isto na configuración do dispositivo.</string>
<string name="device_settings_rename_label">Renomear</string>
<string name="device_settings_rename_hint">Nome do dispositivo</string>
<string name="device_settings_rename_confirm">Renomear</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Configuración de Bluetooth</string>
<string name="device_settings_send_failed">Non se puido aplicar a configuración: %1$s</string>
<string name="device_settings_anc_off_rejected_message">O modo Desactivado non está habilitado neste dispositivo. Activa «Permitir o modo Desactivado» en Control de ruído.</string>
<string name="device_settings_category_battery_label">Batería</string>
<string name="device_settings_charge_cap_label">Límite de carga optimizado</string>
<string name="device_settings_charge_cap_description">Aprende a túa rutina e pausa a carga arredor do 80%% para prolongar a vida da batería, cargando os auriculares antes de que os vaias usar.</string>
<string name="device_settings_charge_cap_rejected_message">Non se puido cambiar o límite de carga optimizado. Esta é unha función experimental; infórmanos se segue fallando.</string>
<string name="device_settings_category_connections_label">Dispositivos conectados</string>
<string name="device_settings_connected_devices_description">Outros dispositivos conectados actualmente a estes AirPods</string>
<string name="device_settings_connected_device_label">Dispositivo %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">पृष्ठभूमि पारदर्शिता</string>
<string name="widget_config_show_device_label">डिवाइस का नाम दिखाएं</string>
<string name="widget_config_reset_label">डिफ़ॉल्ट पर रीसेट करें</string>
<string name="widget_config_preview_resize_hint">आप होम स्क्रीन पर रखने के बाद विजेट का आकार बदल सकते हैं।</string>
<string name="widget_config_custom_label">कस्टम</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">गहरा</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">यह एक अज्ञात डिवाइस है, लेकिन यह समान संदेश प्रारूप का उपयोग कर रहा है। इसके लिए समर्थन जोड़ते हैं, मुझसे संपर्क करें :)</string>
<string name="pods_none_label_short">कोई डिवाइस नहीं</string>
<string name="pods_charging_label">चार्ज हो रहा है</string>
<string name="pods_charging_optimized_label">अनुकूलित</string>
<string name="pods_inear_label">कान में</string>
<string name="pods_microphone_label">माइक्रोफ़ोन</string>
<string name="anc_mode_off">बंद</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth डिवाइस लेबल</string>
<string name="device_settings_info_model_label">मॉडल</string>
<string name="device_settings_info_manufacturer_label">निर्माता</string>
<string name="device_settings_info_hardware_label">हार्डवेयर</string>
<string name="device_settings_info_serial_label">सीरियल नंबर</string>
<string name="device_settings_info_firmware_label">फर्मवेयर</string>
<string name="device_settings_info_firmware_pending_label">लंबित फर्मवेयर</string>
<string name="device_settings_info_build_label">बिल्ड</string>
<string name="device_settings_info_left_serial_label">बाएं पॉड का सीरियल नंबर</string>
<string name="device_settings_info_right_serial_label">दाएं पॉड का सीरियल नंबर</string>
<string name="device_settings_info_left_bonded_label">बायाँ युग्मित</string>
<string name="device_settings_info_right_bonded_label">दायाँ युग्मित</string>
<string name="device_settings_info_details_label">डिवाइस विवरण</string>
<string name="device_settings_info_details_action">डिवाइस विवरण दिखाएं</string>
<string name="device_settings_info_status_label">स्थिति</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">यह डिवाइस पास में है लेकिन इस फ़ोन से कनेक्ट नहीं है। सेटिंग्स और नियंत्रण तक पहुंचने के लिए कनेक्ट करें।</string>
<string name="device_settings_not_connected_connect_action">कनेक्ट करें</string>
<string name="device_settings_not_connected_open_settings_action">ब्लूटूथ सेटिंग्स खोलें</string>
<string name="device_settings_not_nearby_label">डिवाइस पास में नहीं है</string>
<string name="device_settings_not_nearby_description">सेटिंग्स केवल तभी उपलब्ध होती हैं जब यह डिवाइस आपके फ़ोन के पास और उससे कनेक्ट हो।</string>
<string name="device_settings_aap_unavailable_label">उन्नत सेटिंग्स अनुपलब्ध</string>
<string name="device_settings_aap_unavailable_description">उन्नत AirPods सेटिंग के लिए Bluetooth फ़ीचर की आवश्यकता है जो इस फ़ोन पर उपलब्ध नहीं है। आपके डिवाइस निर्माता के भविष्य के Android अपडेट से यह समस्या हल हो सकती है।</string>
<string name="device_settings_aap_unavailable_action">संगतता ट्रैकर देखें</string>
<string name="device_settings_category_sound_label">ध्वनि</string>
<string name="device_settings_category_controls_label">नियंत्रण</string>
<string name="device_settings_nc_one_airpod_label">एक पॉड के साथ ANC</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">माइक्रोफ़ोन म्यूट करने के लिए एक बार दबाएं</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">कॉल समाप्त करने के लिए दो बार दबाएं</string>
<string name="device_settings_open_cd">डिवाइस सेटिंग्स खोलें</string>
<string name="overview_card_missing_paired_device_cd">प्रोफ़ाइल में कोई पेयर किया हुआ ब्लूटूथ डिवाइस नहीं है — प्रोफ़ाइल संपादित करने के लिए टैप करें</string>
<string name="overview_card_missing_paired_device">इस प्रोफ़ाइल में कोई पेयर किया गया Bluetooth डिवाइस नहीं है</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">सामान्य</string>
<string name="device_settings_microphone_mode_label">माइक्रोफ़ोन</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Off को चयनयोग्य शोर नियंत्रण मोड के रूप में अनुमति दें। अक्षम होने पर, स्टेम साइकिल करते समय Off को छोड़ देते हैं।</string>
<string name="device_settings_sleep_detection_label">नींद पहचान</string>
<string name="device_settings_sleep_detection_description">सोने पर स्वचालित रूप से ऑडियो रोकें</string>
<string name="reaction_sleep_channel_label">नींद का पता लगाना</string>
<string name="reaction_sleep_notification_title">नींद का पता लगाने से रोका गया</string>
<string name="reaction_sleep_notification_text">%1$s ने बताया कि आप सो गए, इसलिए आपका संगीत रोक दिया गया। आप इसे डिवाइस सेटिंग्स में बंद कर सकते हैं।</string>
<string name="device_settings_rename_label">नाम बदलें</string>
<string name="device_settings_rename_hint">डिवाइस का नाम</string>
<string name="device_settings_rename_confirm">नाम बदलें</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">ब्लूटूथ सेटिंग्स</string>
<string name="device_settings_send_failed">सेटिंग लागू नहीं हो सकी: %1$s</string>
<string name="device_settings_anc_off_rejected_message">इस डिवाइस पर Off मोड सक्षम नहीं है। शोर नियंत्रण के अंतर्गत \"ऑफ मोड अनुमति दें\" सक्षम करें।</string>
<string name="device_settings_category_battery_label">बैटरी</string>
<string name="device_settings_charge_cap_label">अनुकूलित चार्ज सीमा</string>
<string name="device_settings_charge_cap_description">आपकी दिनचर्या सीखें और बैटरी जीवन बढ़ाने के लिए लगभग 80%% पर चार्जिंग रोकें, इस्तेमाल से पहले पॉड्स को पूरी तरह चार्ज करें।</string>
<string name="device_settings_charge_cap_rejected_message">अनुकूलित चार्ज सीमा नहीं बदली जा सकी। यह एक प्रायोगिक सुविधा है — अगर यह बार-बार विफल होती रहे तो कृपया रिपोर्ट करें।</string>
<string name="device_settings_category_connections_label">कनेक्टेड डिवाइस</string>
<string name="device_settings_connected_devices_description">इन AirPods से वर्तमान में कनेक्टेड अन्य डिवाइस</string>
<string name="device_settings_connected_device_label">डिवाइस %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Prozirnost pozadine</string>
<string name="widget_config_show_device_label">Prikaži naziv uređaja</string>
<string name="widget_config_reset_label">Vrati na zadane vrijednosti</string>
<string name="widget_config_preview_resize_hint">Možete promijeniti veličinu widgeta nakon što ga postavite na početni zaslon.</string>
<string name="widget_config_custom_label">Prilagođeno</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Tamno</string>
@@ -252,6 +253,7 @@
<string name="pods_unknown_contact_dev">Ovo je nepoznat uređaj, ali koristi sličan format poruka. Dodajmo podršku za njega, kontaktirajte me :)</string>
<string name="pods_none_label_short">Nema uređaja</string>
<string name="pods_charging_label">Punjenje</string>
<string name="pods_charging_optimized_label">Optimizirano</string>
<string name="pods_inear_label">U uhu</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Isključeno</string>
@@ -412,11 +414,15 @@
<string name="device_settings_info_bt_name_label">Oznaka Bluetooth uređaja</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">Proizvođač</string>
<string name="device_settings_info_hardware_label">Hardver</string>
<string name="device_settings_info_serial_label">Serijski broj</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmver na čekanju</string>
<string name="device_settings_info_build_label">Verzija</string>
<string name="device_settings_info_left_serial_label">Serijski broj lijevog poda</string>
<string name="device_settings_info_right_serial_label">Serijski broj desnog poda</string>
<string name="device_settings_info_left_bonded_label">Lijevo vezano</string>
<string name="device_settings_info_right_bonded_label">Desno vezano</string>
<string name="device_settings_info_details_label">Detalji uređaja</string>
<string name="device_settings_info_details_action">Prikaži detalje uređaja</string>
<string name="device_settings_info_status_label">Stanje</string>
@@ -426,8 +432,11 @@
<string name="device_settings_not_connected_description">Ovaj uređaj je u blizini, ali nije povezan s ovim telefonom. Povežite se za pristup postavkama i upravljačkim elementima.</string>
<string name="device_settings_not_connected_connect_action">Poveži</string>
<string name="device_settings_not_connected_open_settings_action">Otvori Bluetooth postavke</string>
<string name="device_settings_not_nearby_label">Uređaj nije u blizini</string>
<string name="device_settings_not_nearby_description">Postavke su dostupne samo kada je ovaj uređaj u blizini i spojen na vaš telefon.</string>
<string name="device_settings_aap_unavailable_label">Napredne postavke nedostupne</string>
<string name="device_settings_aap_unavailable_description">Napredne AirPods postavke zahtijevaju Bluetooth značajku koja nije dostupna na ovom telefonu. To može biti riješeno budućim Android ažuriranjem vašeg proizvođača uređaja.</string>
<string name="device_settings_aap_unavailable_action">Pogledajte tracker kompatibilnosti</string>
<string name="device_settings_category_sound_label">Zvuk</string>
<string name="device_settings_category_controls_label">Kontrole</string>
<string name="device_settings_nc_one_airpod_label">ANC s jednim podom</string>
@@ -465,7 +474,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Jedan pritisak za isključivanje mikrofona</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Dvostruki pritisak za završetak poziva</string>
<string name="device_settings_open_cd">Otvori postavke uređaja</string>
<string name="overview_card_missing_paired_device_cd">Profil nema uparen Bluetooth uređaj — dodirnite za uređivanje profila</string>
<string name="overview_card_missing_paired_device">Ovaj profil nema uparenog Bluetooth uređaja.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Općenito</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -488,6 +497,9 @@
<string name="device_settings_allow_off_description">Dozvoli Isključeno kao odabrani način kontrole buke. Kada je onemogućeno, stabljike preskakovanjem Isključeno pri cikliranju.</string>
<string name="device_settings_sleep_detection_label">Detekcija sna</string>
<string name="device_settings_sleep_detection_description">Automatski pauziraj zvuk kad zaspite</string>
<string name="reaction_sleep_channel_label">Otkrivanje spavanja</string>
<string name="reaction_sleep_notification_title">Pauzirano otkrivanjem spavanja</string>
<string name="reaction_sleep_notification_text">%1$s je prijavio da ste zaspali, pa je vaša glazba pauzirana. Možete to onemogućiti u postavkama uređaja.</string>
<string name="device_settings_rename_label">Preimenuj</string>
<string name="device_settings_rename_hint">Naziv uređaja</string>
<string name="device_settings_rename_confirm">Preimenuj</string>
@@ -497,6 +509,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth postavke</string>
<string name="device_settings_send_failed">Nije moguće primijeniti postavku: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Način rada Isključeno nije omogućen na ovom uređaju. Omogućite \"Dozvoli način rada Isključeno\" pod Kontrola buke.</string>
<string name="device_settings_category_battery_label">Baterija</string>
<string name="device_settings_charge_cap_label">Optimizirano ograničenje punjenja</string>
<string name="device_settings_charge_cap_description">Nauči vašu rutinu i pauzira punjenje oko 80%% kako bi produžilo vijek trajanja baterije, dopunjujući slušalice prije nego što ćete ih vjerojatno koristiti.</string>
<string name="device_settings_charge_cap_rejected_message">Optimizirano ograničenje punjenja nije se moglo promijeniti. Ovo je eksperimentalna značajka — molimo prijavite ako se nastavlja neuspješno.</string>
<string name="device_settings_category_connections_label">Povezani uređaji</string>
<string name="device_settings_connected_devices_description">Drugi uređaji koji su trenutno spojeni na ove AirPodove</string>
<string name="device_settings_connected_device_label">Uređaj %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Háttér átlátszósága</string>
<string name="widget_config_show_device_label">Eszköznév megjelenítése</string>
<string name="widget_config_reset_label">Alapértékek visszaállítása</string>
<string name="widget_config_preview_resize_hint">A widgetet átméretezheted, miután elhelyezted a kezdőképernyőn.</string>
<string name="widget_config_custom_label">Egyéni</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Sötét</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Ez egy ismeretlen eszköz, de hasonló üzenetformátumot használ. Adjunk hozzá támogatást, lépjen kapcsolatba velem :)</string>
<string name="pods_none_label_short">Nincs eszköz</string>
<string name="pods_charging_label">Töltés</string>
<string name="pods_charging_optimized_label">Optimalizált</string>
<string name="pods_inear_label">Fülben</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Ki</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth-eszköz neve</string>
<string name="device_settings_info_model_label">Modell</string>
<string name="device_settings_info_manufacturer_label">Gyártó</string>
<string name="device_settings_info_hardware_label">Hardver</string>
<string name="device_settings_info_serial_label">Sorozatszám</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Függőben lévő firmware</string>
<string name="device_settings_info_build_label">Build</string>
<string name="device_settings_info_left_serial_label">Bal fülhallgató sorozatszáma</string>
<string name="device_settings_info_right_serial_label">Jobb fülhallgató sorozatszáma</string>
<string name="device_settings_info_left_bonded_label">Bal fülhallgató párosítva</string>
<string name="device_settings_info_right_bonded_label">Jobb fülhallgató párosítva</string>
<string name="device_settings_info_details_label">Eszköz részletei</string>
<string name="device_settings_info_details_action">Eszköz részleteinek megjelenítése</string>
<string name="device_settings_info_status_label">Állapot</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Ez az eszköz a közelben van, de nincs csatlakoztatva ehhez a telefonhoz. Csatlakozzon a beállítások és vezérlők eléréséhez.</string>
<string name="device_settings_not_connected_connect_action">Csatlakozás</string>
<string name="device_settings_not_connected_open_settings_action">Bluetooth beállítások megnyitása</string>
<string name="device_settings_not_nearby_label">Az eszköz nincs a közelben</string>
<string name="device_settings_not_nearby_description">A beállítások csak akkor érhetők el, ha az eszköz a közelben van és csatlakoztatva van a telefonodhoz.</string>
<string name="device_settings_aap_unavailable_label">Speciális beállítások nem elérhetők</string>
<string name="device_settings_aap_unavailable_description">A speciális AirPods beállításokhoz olyan Bluetooth-funkció szükséges, amely nem érhető el ezen a telefonon. Ez megoldódhat a készülékgyártó jövőbeli Android-frissítésével.</string>
<string name="device_settings_aap_unavailable_action">Kompatibilitási lista megtekintése</string>
<string name="device_settings_category_sound_label">Hang</string>
<string name="device_settings_category_controls_label">Vezérlők</string>
<string name="device_settings_nc_one_airpod_label">ANC egyetlen fülhallgatóval</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Egyszeri nyomás a mikrofon némításához</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Dupla nyomás a hívás befejezéséhez</string>
<string name="device_settings_open_cd">Eszközbeállítások megnyitása</string>
<string name="overview_card_missing_paired_device_cd">A profilhoz nincs párosított Bluetooth-eszköz koppintson a profil szerkesztéséhez</string>
<string name="overview_card_missing_paired_device">Ehhez a profilhoz nincs párosított Bluetooth-eszköz.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Általános</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">A Ki mód engedélyezése választható zajvezérlési módként. Ha le van tiltva, a szárak kihagyják a Ki módot a ciklizálás során.</string>
<string name="device_settings_sleep_detection_label">Alvásérzékelés</string>
<string name="device_settings_sleep_detection_description">Audíó automatikus szüneteltetéseelalváskor</string>
<string name="reaction_sleep_channel_label">Alvásérzékelés</string>
<string name="reaction_sleep_notification_title">Az alvásérzékelés által szüneteltetve</string>
<string name="reaction_sleep_notification_text">%1$s jelezte, hogy elaludtál, ezért a zene szünetelt. Ezt a készülék beállításaiban kapcsolhatod ki.</string>
<string name="device_settings_rename_label">Átnevézés</string>
<string name="device_settings_rename_hint">Eszköz neve</string>
<string name="device_settings_rename_confirm">Átnevézés</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth-beállítások</string>
<string name="device_settings_send_failed">Nem sikerült alkalmazni a beállítást: %1$s</string>
<string name="device_settings_anc_off_rejected_message">A Ki mód nincs engedélyezve ezen az eszközön. Engedélyezd a \"Ki mód engedélyezése\" opciót a Zajvezérlés alatt.</string>
<string name="device_settings_category_battery_label">Akkumulátor</string>
<string name="device_settings_charge_cap_label">Optimalizált töltési limit</string>
<string name="device_settings_charge_cap_description">Megtanulja a szokásaidat, és kb. 80%%-nál szünetelteti a töltést az akkumulátor élettartalmának megnyújtásához, majd befejezi a töltést, mielőtt valószínűleg használni szeretnéd.</string>
<string name="device_settings_charge_cap_rejected_message">Az optimalizált töltési limit módosítása nem sikerült. Ez egy kísérleti funkció — kérjük, jelezd, ha továbbra is hibás marad.</string>
<string name="device_settings_category_connections_label">Csatlakoztatott eszközök</string>
<string name="device_settings_connected_devices_description">Más eszközök, amelyek jelenleg csatlakoznak ezekhez az AirPodokhoz</string>
<string name="device_settings_connected_device_label">%d. eszköz</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Ֆոնի թափանցիկություն</string>
<string name="widget_config_show_device_label">Ցուցադրել սարքի անունը</string>
<string name="widget_config_reset_label">Վերականգնել լռելյալը</string>
<string name="widget_config_preview_resize_hint">Դուք կարող եք փոխել վիջեթի չափը այն ձեր գլխավոր էկրանին տեղադրելուց հետո:</string>
<string name="widget_config_custom_label">Հատուկ</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Մութ</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Սա անհայտ սարք է, բայց այն օգտագործում է նմանատիպ հաղորդագրության ձևաչափ։ Եկեք ավելացնենք աջակցություն դրա համար, կապվեք ինձ հետ :)</string>
<string name="pods_none_label_short">Սարք չկա</string>
<string name="pods_charging_label">Լիցքավորվում է</string>
<string name="pods_charging_optimized_label">Օպտիմիզացված</string>
<string name="pods_inear_label">Ականջում է</string>
<string name="pods_microphone_label">Բարձրախոս</string>
<string name="anc_mode_off">Անջատ</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth սարքի անուն</string>
<string name="device_settings_info_model_label">Մոդել</string>
<string name="device_settings_info_manufacturer_label">Արտադրող</string>
<string name="device_settings_info_hardware_label">Ապարատ</string>
<string name="device_settings_info_serial_label">Սերիական համար</string>
<string name="device_settings_info_firmware_label">Ծրագրաշար</string>
<string name="device_settings_info_firmware_pending_label">Սպասվող որոնվածային ծրագիր</string>
<string name="device_settings_info_build_label">Կառուցվածք</string>
<string name="device_settings_info_left_serial_label">Ձախ ականջակալի սերիա</string>
<string name="device_settings_info_right_serial_label">Աջ ականջակալի սերիա</string>
<string name="device_settings_info_left_bonded_label">Ձախ կապակցված</string>
<string name="device_settings_info_right_bonded_label">Աջ կապակցված</string>
<string name="device_settings_info_details_label">Սարքի մանրամասները</string>
<string name="device_settings_info_details_action">Ցուցադրել սարքի մանրամասները</string>
<string name="device_settings_info_status_label">Կարգավիճակ</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Սարքը մոտ է, սակայն կապված չէ այս հեռախոսին։ Կապվեք՝ կարգավորումներն ու կառավարումները մուտք գործելու համար։</string>
<string name="device_settings_not_connected_connect_action">Կապվել</string>
<string name="device_settings_not_connected_open_settings_action">Բացել Bluetooth-ի կարգավորումները</string>
<string name="device_settings_not_nearby_label">Սարքը մոտակայքում չէ</string>
<string name="device_settings_not_nearby_description">Կարգավորումները հասանելի են միայն այն դեպքում, երբ այս սարքը մոտակայքում է և միացած ձեր հեռախոսին։</string>
<string name="device_settings_aap_unavailable_label">Ընդլայնված կարգաբերումներն անհասանելի են</string>
<string name="device_settings_aap_unavailable_description">AirPods-ի առաջադեմ կարգավորումները պահանջում են Bluetooth հատկություն, որը հասանելի չէ այս հեռախոսին: Այս կարգը կարող է լուծվել ձեր սարքի արտադրողի Android-ի ապագա թարմացմամբ:</string>
<string name="device_settings_aap_unavailable_action">Դիտել համատեղելիության հետագծիչը</string>
<string name="device_settings_category_sound_label">Ձայն</string>
<string name="device_settings_category_controls_label">Կառավարում</string>
<string name="device_settings_nc_one_airpod_label">ANC մեկ ականջակալով</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Մեկ սեղմում խոսափոնը անձայնեցնելու համար</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Կրկնակի սեղմում զանգը ավարտելու համար</string>
<string name="device_settings_open_cd">Բացել սառքի կառավարումները</string>
<string name="overview_card_missing_paired_device_cd">Պրոֆիլն կցված Bluetooth-սարք չունի — հպեք պրոֆիլը խմբագրելու համար</string>
<string name="overview_card_missing_paired_device">Այս պրոֆիլը չունի զուգակցված Bluetooth սարք:</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Ընդհանուր</string>
<string name="device_settings_microphone_mode_label">Խոսափող</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Թույլատրել Անջատ-ը որպես ընտրովի աղմուկի կառավարման ռեժիմ: Անջատելու դեպքում կառուները շրջանի ժամանակ բաց են թողնում Անջատ-ը:</string>
<string name="device_settings_sleep_detection_label">Քնի հայտնաբերում</string>
<string name="device_settings_sleep_detection_description">Ավտոմատ կասեցնել ձայնը, երբ քնում եք</string>
<string name="reaction_sleep_channel_label">Քնի հայտնաբերում</string>
<string name="reaction_sleep_notification_title">Դադարեցված Քնի հայտնաբերման կողմից</string>
<string name="reaction_sleep_notification_text">%1$s-ը հայտնել է, որ դուք քնեցիք, ուստի ձեր երաժշտությունը դադարեցվեց։ Դուք կարող եք անջատել սա սարքի կարգավորումներում։</string>
<string name="device_settings_rename_label">Փոխել անունը</string>
<string name="device_settings_rename_hint">Սառքի անուն</string>
<string name="device_settings_rename_confirm">Փոխել անունը</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth կարգաբերումներ</string>
<string name="device_settings_send_failed">Չհաջողվեց կիրառել կարգավորումը՝ %1$s</string>
<string name="device_settings_anc_off_rejected_message">Անջատ ռեժիմը հասանելի չէ այս սարքին: Միացրեք «Թույլատրել Անջատ ռեժիմը» Աղմուկի կառավարման բաժնում:</string>
<string name="device_settings_category_battery_label">Մարտկոց</string>
<string name="device_settings_charge_cap_label">Լիցքի օպտիմիզացված սահման</string>
<string name="device_settings_charge_cap_description">Սովորեք ձեր ռեժիմը և դադարեցրեք լիցքավորումը մոտ 80%%-ի դեպքում՝ մարտկոցի ծառայության ժամկետը երկարացնելու համար՝ pods-երը լիցքավորելով նախքան դրանք օգտագործելը։</string>
<string name="device_settings_charge_cap_rejected_message">Լիցքի օպտիմիզացված սահմանը հնարավոր չեղավ փոխել։ Սա փորձարարական հատկություն է — խնդրում ենք հայտնել, եթե այն շարունակի ձախողվել։</string>
<string name="device_settings_category_connections_label">Կապլված սառքեր</string>
<string name="device_settings_connected_devices_description">Այլ սառքերը, որոնք կապլված են խի AirPods-ին</string>
<string name="device_settings_connected_device_label">Սառք %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Transparansi latar belakang</string>
<string name="widget_config_show_device_label">Tampilkan nama perangkat</string>
<string name="widget_config_reset_label">Atur ulang ke default</string>
<string name="widget_config_preview_resize_hint">Anda dapat mengubah ukuran widget setelah menempatkannya di layar beranda.</string>
<string name="widget_config_custom_label">Kustom</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Gelap</string>
@@ -248,6 +249,7 @@
<string name="pods_unknown_contact_dev">Ini adalah perangkat yang tidak dikenal, tetapi menggunakan format pesan yang serupa. Mari tambahkan dukungannya, hubungi saya :)</string>
<string name="pods_none_label_short">Tidak ada perangkat</string>
<string name="pods_charging_label">Mengisi daya</string>
<string name="pods_charging_optimized_label">Dioptimalkan</string>
<string name="pods_inear_label">Di telinga</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="anc_mode_off">Nonaktif</string>
@@ -402,11 +404,15 @@
<string name="device_settings_info_bt_name_label">Label Perangkat Bluetooth</string>
<string name="device_settings_info_model_label">Model</string>
<string name="device_settings_info_manufacturer_label">Produsen</string>
<string name="device_settings_info_hardware_label">Perangkat Keras</string>
<string name="device_settings_info_serial_label">Nomor Seri</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware Tertunda</string>
<string name="device_settings_info_build_label">Build</string>
<string name="device_settings_info_left_serial_label">Nomor Seri Pod Kiri</string>
<string name="device_settings_info_right_serial_label">Nomor Seri Pod Kanan</string>
<string name="device_settings_info_left_bonded_label">Kiri Terhubung</string>
<string name="device_settings_info_right_bonded_label">Kanan Terhubung</string>
<string name="device_settings_info_details_label">Detail Perangkat</string>
<string name="device_settings_info_details_action">Tampilkan detail perangkat</string>
<string name="device_settings_info_status_label">Status</string>
@@ -416,8 +422,11 @@
<string name="device_settings_not_connected_description">Perangkat ini ada di dekat sini tetapi tidak terhubung ke ponsel ini. Hubungkan untuk mengakses pengaturan dan kontrol.</string>
<string name="device_settings_not_connected_connect_action">Hubungkan</string>
<string name="device_settings_not_connected_open_settings_action">Buka Pengaturan Bluetooth</string>
<string name="device_settings_not_nearby_label">Perangkat tidak di dekatnya</string>
<string name="device_settings_not_nearby_description">Pengaturan hanya tersedia saat perangkat ini berada di dekatnya dan terhubung ke ponsel Anda.</string>
<string name="device_settings_aap_unavailable_label">Pengaturan lanjutan tidak tersedia</string>
<string name="device_settings_aap_unavailable_description">Pengaturan AirPods lanjutan memerlukan fitur Bluetooth yang tidak tersedia di ponsel ini. Masalah ini mungkin akan diselesaikan oleh pembaruan Android dari produsen perangkat Anda di masa mendatang.</string>
<string name="device_settings_aap_unavailable_action">Lihat pelacak kompatibilitas</string>
<string name="device_settings_category_sound_label">Suara</string>
<string name="device_settings_category_controls_label">Kontrol</string>
<string name="device_settings_nc_one_airpod_label">ANC dengan satu pod</string>
@@ -455,7 +464,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Tekan sekali untuk membisukan mikrofon</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Tekan dua kali untuk mengakhiri panggilan</string>
<string name="device_settings_open_cd">Buka pengaturan perangkat</string>
<string name="overview_card_missing_paired_device_cd">Profil tidak memiliki Perangkat Bluetooth yang dipasangkan — ketuk untuk mengedit profil</string>
<string name="overview_card_missing_paired_device">Profil ini tidak memiliki Perangkat Bluetooth yang dipasangkan.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Umum</string>
<string name="device_settings_microphone_mode_label">Mikrofon</string>
@@ -478,6 +487,9 @@
<string name="device_settings_allow_off_description">Izinkan Mati sebagai mode kontrol kebisingan yang dapat dipilih. Jika dinonaktifkan, stem melewati Mati saat bersiklus.</string>
<string name="device_settings_sleep_detection_label">Deteksi Tidur</string>
<string name="device_settings_sleep_detection_description">Otomatis jeda audio saat Anda tertidur</string>
<string name="reaction_sleep_channel_label">Deteksi Tidur</string>
<string name="reaction_sleep_notification_title">Dijeda oleh Deteksi Tidur</string>
<string name="reaction_sleep_notification_text">%1$s mendeteksi Anda tertidur, sehingga musik Anda dijeda. Anda dapat menonaktifkan ini di pengaturan perangkat.</string>
<string name="device_settings_rename_label">Ganti Nama</string>
<string name="device_settings_rename_hint">Nama perangkat</string>
<string name="device_settings_rename_confirm">Ganti Nama</string>
@@ -487,6 +499,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Pengaturan Bluetooth</string>
<string name="device_settings_send_failed">Tidak dapat menerapkan pengaturan: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Mode Mati tidak diaktifkan di perangkat ini. Aktifkan \"Izinkan mode Mati\" di bawah Kontrol Kebisingan.</string>
<string name="device_settings_category_battery_label">Baterai</string>
<string name="device_settings_charge_cap_label">Batas Pengisian Teroptimasi</string>
<string name="device_settings_charge_cap_description">Mempelajari rutinitas Anda dan menjeda pengisian sekitar 80%% untuk memperpanjang masa pakai baterai, mengisi pod sebelum Anda kemungkinan menggunakannya.</string>
<string name="device_settings_charge_cap_rejected_message">Batas Pengisian Teroptimasi tidak dapat diubah. Ini adalah fitur eksperimental — laporkan jika terus gagal.</string>
<string name="device_settings_category_connections_label">Perangkat Terhubung</string>
<string name="device_settings_connected_devices_description">Perangkat lain yang saat ini terhubung ke AirPods ini</string>
<string name="device_settings_connected_device_label">Perangkat %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Gagnsæi bakgrunns</string>
<string name="widget_config_show_device_label">Sýna heiti tækis</string>
<string name="widget_config_reset_label">Endurstilla sjálfgildi</string>
<string name="widget_config_preview_resize_hint">Þú getur breytt stærð græjunnar eftir að þú hefur sett hana á heimaskjáinn þinn.</string>
<string name="widget_config_custom_label">Sérsniðið</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Dökkt</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Þetta er óþekkt tæki, en það notar svipað skilaboðasnið. Bætum við stuðningi fyrir það, hafðu samband :)</string>
<string name="pods_none_label_short">Ekkert tæki</string>
<string name="pods_charging_label">Hleður</string>
<string name="pods_charging_optimized_label">Fínstillt</string>
<string name="pods_inear_label">Í eyra</string>
<string name="pods_microphone_label">Hljóðnemi</string>
<string name="anc_mode_off">Slökkt</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Bluetooth-tækjaheiti</string>
<string name="device_settings_info_model_label">Líkan</string>
<string name="device_settings_info_manufacturer_label">Framleiðandi</string>
<string name="device_settings_info_hardware_label">Vélbúnaður</string>
<string name="device_settings_info_serial_label">Raðnúmer</string>
<string name="device_settings_info_firmware_label">Hugbúnnaður</string>
<string name="device_settings_info_firmware_pending_label">Hugbúnaðaruppfærsla í bið</string>
<string name="device_settings_info_build_label">Útgáfa</string>
<string name="device_settings_info_left_serial_label">Raðnúmer vinstri hlustunarpípu</string>
<string name="device_settings_info_right_serial_label">Raðnúmer hægri hlustunarpípu</string>
<string name="device_settings_info_left_bonded_label">Vinstri tengd</string>
<string name="device_settings_info_right_bonded_label">Hægri tengd</string>
<string name="device_settings_info_details_label">Tækjaupplýsingar</string>
<string name="device_settings_info_details_action">Sýna tækjaupplýsingar</string>
<string name="device_settings_info_status_label">Staða</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Þetta tæki er í nágrenninu en er ekki tengt þessum síma. Tengdu til að fá aðgang að stillingum og stjórnun.</string>
<string name="device_settings_not_connected_connect_action">Tengja</string>
<string name="device_settings_not_connected_open_settings_action">Opna Bluetooth stillingar</string>
<string name="device_settings_not_nearby_label">Tækið ekki í nágrenninu</string>
<string name="device_settings_not_nearby_description">Stillingar eru aðeins tiltækar þegar þetta tæki er í nágrenninu og tengt við símann þinn.</string>
<string name="device_settings_aap_unavailable_label">Ítarlegar stillingar ekki tiltækar</string>
<string name="device_settings_aap_unavailable_description">Ítarlegar AirPods-stillingar krefjast Bluetooth-eiginleika sem er ekki tiltækur á þessum síma. Þetta gæti lagast með framtiðaruppfærslu Android frá framleiðanda tækisins þíns.</string>
<string name="device_settings_aap_unavailable_action">Skoða samhæfnirakara</string>
<string name="device_settings_category_sound_label">Hljóð</string>
<string name="device_settings_category_controls_label">Stýring</string>
<string name="device_settings_nc_one_airpod_label">ANC með einni hlustunarpípu</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Eitt þrýsting til að þagga hátalara</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Tvöföld þrýsting til að ljúka simátali</string>
<string name="device_settings_open_cd">Opna tæki stillingar</string>
<string name="overview_card_missing_paired_device_cd">Prófíll á engan tengdan Bluetooth-tæki — ýttu til að breyta prófíl</string>
<string name="overview_card_missing_paired_device">Þetta snið á sér ekkert tengt Bluetooth-tæki.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Álmennt</string>
<string name="device_settings_microphone_mode_label">Hljóðnemi</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Leyfa Af sem valanlega hljóðstýringunarstillingu. Þegar slökt er á þessu sleppir stillingarpinninn Af í hringrás.</string>
<string name="device_settings_sleep_detection_label">Svefngreining</string>
<string name="device_settings_sleep_detection_description">Gera hljóð hljóðlegt sjálfvirkt þá sem þu sofnar</string>
<string name="reaction_sleep_channel_label">Svefngreining</string>
<string name="reaction_sleep_notification_title">Gert hlé af svefngreiningu</string>
<string name="reaction_sleep_notification_text">%1$s greindi að þú sefur, þannig var tónlistin sett í hlé. Þú getur slökkt á þessu í tækjastillingum.</string>
<string name="device_settings_rename_label">Endurskíra</string>
<string name="device_settings_rename_hint">Nafn tækis</string>
<string name="device_settings_rename_confirm">Endurskíra</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth-stillingar</string>
<string name="device_settings_send_failed">Gat ekki beitt stillingu: %1$s</string>
<string name="device_settings_anc_off_rejected_message">Af-stilling er ekki virk á þessu tæki. Virkjaðu „Af-stilling leyfist“ undir Hljóðstýring.</string>
<string name="device_settings_category_battery_label">Rafhlaða</string>
<string name="device_settings_charge_cap_label">Fínstillt hleðslutakmark</string>
<string name="device_settings_charge_cap_description">Lærir venjur þínar og hleyðir að 80%% til að lengja líftíma rafhlöðunnar, og lokar hleðslunni áður en þú notar þær.</string>
<string name="device_settings_charge_cap_rejected_message">Fínstilltu hleðslutakmarkið gat ekki verið breytt. Þetta er tilraunaeiginleiki — vinsamlegast tilkynntu ef hann heldur áfram að mistakast.</string>
<string name="device_settings_category_connections_label">Tengd tæki</string>
<string name="device_settings_connected_devices_description">Onnur tæki sem eru tengd við þessa AirPods</string>
<string name="device_settings_connected_device_label">Tæki %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">Trasparenza sfondo</string>
<string name="widget_config_show_device_label">Mostra nome dispositivo</string>
<string name="widget_config_reset_label">Ripristina predefiniti</string>
<string name="widget_config_preview_resize_hint">Puoi ridimensionare il widget dopo averlo posizionato sulla schermata Home.</string>
<string name="widget_config_custom_label">Personalizzato</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">Scuro</string>
@@ -250,6 +251,7 @@
<string name="pods_unknown_contact_dev">Questo è un dispositivo sconosciuto, ma utilizza un formato di messaggio simile. Aggiungiamo il supporto, contattami :)</string>
<string name="pods_none_label_short">Nessun dispositivo</string>
<string name="pods_charging_label">In carica</string>
<string name="pods_charging_optimized_label">Ottimizzato</string>
<string name="pods_inear_label">Nell\'orecchio</string>
<string name="pods_microphone_label">Microfono</string>
<string name="anc_mode_off">Spento</string>
@@ -407,11 +409,15 @@
<string name="device_settings_info_bt_name_label">Etichetta dispositivo Bluetooth</string>
<string name="device_settings_info_model_label">Modello</string>
<string name="device_settings_info_manufacturer_label">Produttore</string>
<string name="device_settings_info_hardware_label">Hardware</string>
<string name="device_settings_info_serial_label">Numero di serie</string>
<string name="device_settings_info_firmware_label">Firmware</string>
<string name="device_settings_info_firmware_pending_label">Firmware in attesa</string>
<string name="device_settings_info_build_label">Build</string>
<string name="device_settings_info_left_serial_label">Seriale pod sinistro</string>
<string name="device_settings_info_right_serial_label">Seriale pod destro</string>
<string name="device_settings_info_left_bonded_label">Sinistra associata</string>
<string name="device_settings_info_right_bonded_label">Destra associata</string>
<string name="device_settings_info_details_label">Dettagli dispositivo</string>
<string name="device_settings_info_details_action">Mostra dettagli dispositivo</string>
<string name="device_settings_info_status_label">Stato</string>
@@ -421,8 +427,11 @@
<string name="device_settings_not_connected_description">Questo dispositivo è nelle vicinanze ma non è connesso a questo telefono. Connettiti per accedere alle impostazioni e ai controlli.</string>
<string name="device_settings_not_connected_connect_action">Connetti</string>
<string name="device_settings_not_connected_open_settings_action">Apri Impostazioni Bluetooth</string>
<string name="device_settings_not_nearby_label">Dispositivo non nelle vicinanze</string>
<string name="device_settings_not_nearby_description">Le impostazioni sono disponibili solo quando questo dispositivo è nelle vicinanze e connesso al tuo telefono.</string>
<string name="device_settings_aap_unavailable_label">Impostazioni avanzate non disponibili</string>
<string name="device_settings_aap_unavailable_description">Le impostazioni avanzate degli AirPods richiedono una funzione Bluetooth non disponibile su questo telefono. Potrebbe essere risolta con un futuro aggiornamento Android da parte del produttore del dispositivo.</string>
<string name="device_settings_aap_unavailable_action">Visualizza il tracker di compatibilità</string>
<string name="device_settings_category_sound_label">Audio</string>
<string name="device_settings_category_controls_label">Controlli</string>
<string name="device_settings_nc_one_airpod_label">ANC con un solo pod</string>
@@ -460,7 +469,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">Premere una volta per disattivare il microfono</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">Premere due volte per terminare la chiamata</string>
<string name="device_settings_open_cd">Apri impostazioni dispositivo</string>
<string name="overview_card_missing_paired_device_cd">Il profilo non ha un dispositivo Bluetooth associato — tocca per modificare il profilo</string>
<string name="overview_card_missing_paired_device">Questo profilo non ha nessun dispositivo Bluetooth associato.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">Generale</string>
<string name="device_settings_microphone_mode_label">Microfono</string>
@@ -483,6 +492,9 @@
<string name="device_settings_allow_off_description">Consenti Off come modalità di controllo del rumore selezionabile. Se disabilitato, i gambi saltano Off durante il ciclo.</string>
<string name="device_settings_sleep_detection_label">Rilevamento sonno</string>
<string name="device_settings_sleep_detection_description">Metti automaticamente in pausa laudio quando ti addormenti</string>
<string name="reaction_sleep_channel_label">Rilevamento del sonno</string>
<string name="reaction_sleep_notification_title">In pausa per Rilevamento del sonno</string>
<string name="reaction_sleep_notification_text">%1$s ha rilevato che ti sei addormentato/a, quindi la musica è stata messa in pausa. Puoi disabilitarlo nelle impostazioni del dispositivo.</string>
<string name="device_settings_rename_label">Rinomina</string>
<string name="device_settings_rename_hint">Nome dispositivo</string>
<string name="device_settings_rename_confirm">Rinomina</string>
@@ -492,6 +504,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Impostazioni Bluetooth</string>
<string name="device_settings_send_failed">Impossibile applicare l\'impostazione: %1$s</string>
<string name="device_settings_anc_off_rejected_message">La modalità Off non è abilitata su questo dispositivo. Abilita \"Consenti modalità Off\" sotto Controllo del rumore.</string>
<string name="device_settings_category_battery_label">Batteria</string>
<string name="device_settings_charge_cap_label">Limite di carica ottimizzato</string>
<string name="device_settings_charge_cap_description">Impara la tua routine e metti in pausa la ricarica intorno all\'80%% per prolungare la durata della batteria, completando la carica degli auricolari prima che tu li utilizzi.</string>
<string name="device_settings_charge_cap_rejected_message">Il limite di carica ottimizzato non è stato modificato. Si tratta di una funzionalità sperimentale — segnala il problema se continua a non funzionare.</string>
<string name="device_settings_category_connections_label">Dispositivi connessi</string>
<string name="device_settings_connected_devices_description">Altri dispositivi attualmente connessi a questi AirPods</string>
<string name="device_settings_connected_device_label">Dispositivo %d</string>
+23 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">שקיפות רקע</string>
<string name="widget_config_show_device_label">הצג שם התקן</string>
<string name="widget_config_reset_label">אפס לברירות המחדל</string>
<string name="widget_config_preview_resize_hint">ניתן לשנות את גודל הווידג\'ט לאחר הצבתו על מסך הבית שלך.</string>
<string name="widget_config_custom_label">מותאם אישית</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">כהה</string>
@@ -204,6 +205,12 @@
<item quantity="many">%d מכשירים ללא פרופיל תואם</item>
<item quantity="other">%d מכשירים ללא פרופיל תואם</item>
</plurals>
<plurals name="overview_more_devices_upgrade">
<item quantity="one">התקן %d נוסף</item>
<item quantity="two">%d התקנים נוספים</item>
<item quantity="many">%d התקנים נוספים</item>
<item quantity="other">%d התקנים נוספים</item>
</plurals>
<string name="permission_bluetooth_connect_label">חיבור Bluetooth</string>
<string name="permission_bluetooth_connect_description">אפליקציה זו דורשת הרשאת \"חיבור Bluetooth\" כדי לתקשר עם מכשירים מותאמים וליזום חיבורים.</string>
<string name="permission_bluetooth_scan_label">סריקת Bluetooth</string>
@@ -248,6 +255,7 @@
<string name="pods_unknown_contact_dev">זהו מכשיר לא ידוע, אך הוא משתמש בפורמט הודעות דומה. בואו נוסיף תמיכה בו, צרו איתי קשר :)</string>
<string name="pods_none_label_short">אין מכשיר</string>
<string name="pods_charging_label">טעינה</string>
<string name="pods_charging_optimized_label">מותאם</string>
<string name="pods_inear_label">באוזן</string>
<string name="pods_microphone_label">מיקרופון</string>
<string name="anc_mode_off">כבוי</string>
@@ -411,11 +419,15 @@
<string name="device_settings_info_bt_name_label">תווית התקן בלוטות\'</string>
<string name="device_settings_info_model_label">דגם</string>
<string name="device_settings_info_manufacturer_label">יצרן</string>
<string name="device_settings_info_hardware_label">חומרה</string>
<string name="device_settings_info_serial_label">מספר סריאלי</string>
<string name="device_settings_info_firmware_label">קושחאר</string>
<string name="device_settings_info_firmware_pending_label">קושחה ממתינה</string>
<string name="device_settings_info_build_label">גרסה</string>
<string name="device_settings_info_left_serial_label">מספר סידורי שמאל</string>
<string name="device_settings_info_right_serial_label">מספר סידורי ימין</string>
<string name="device_settings_info_left_bonded_label">שמאל מחובר</string>
<string name="device_settings_info_right_bonded_label">ימין מחובר</string>
<string name="device_settings_info_details_label">פרטי התקן</string>
<string name="device_settings_info_details_action">הצג פרטי התקן</string>
<string name="device_settings_info_status_label">מצב</string>
@@ -425,8 +437,11 @@
<string name="device_settings_not_connected_description">המכשיר בקרבת מקום אך אינו מחובר לטלפון זה. התחבר כדי לגשת להגדרות ופקדים.</string>
<string name="device_settings_not_connected_connect_action">התחבר</string>
<string name="device_settings_not_connected_open_settings_action">פתח הגדרות Bluetooth</string>
<string name="device_settings_not_nearby_label">המכשיר אינו בקרבת מקום</string>
<string name="device_settings_not_nearby_description">ההגדרות זמינות רק כאשר המכשיר נמצא בקרבת מקום ומחובר לטלפון שלך.</string>
<string name="device_settings_aap_unavailable_label">הגדרות מתקדמות אינן זמינות</string>
<string name="device_settings_aap_unavailable_description">הגדרות AirPods מתקדמות דורשות תכונת בלוטות\' שאינה זמינה בטלפון זה. ייתכן שיש לכך פתרון בעדכון אנדרואיד עתידי מיצרן המכשיר.</string>
<string name="device_settings_aap_unavailable_action">צפה במעקב תאימות</string>
<string name="device_settings_category_sound_label">צליל</string>
<string name="device_settings_category_controls_label">בקרות</string>
<string name="device_settings_nc_one_airpod_label">ANC עם אוזנייה אחת</string>
@@ -464,7 +479,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">לחיצה אחת להשתקת מיקרופון</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">לחיצה כפולה לסיום שיחה</string>
<string name="device_settings_open_cd">פתח הגדרות מכשיר</string>
<string name="overview_card_missing_paired_device_cd">לפרופיל אין מכשיר Bluetooth מצומד — הקש לעריכת הפרופיל</string>
<string name="overview_card_missing_paired_device">לפרופיל זה אין התקן Bluetooth מקושר.</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">כללי</string>
<string name="device_settings_microphone_mode_label">מיקרופון</string>
@@ -487,6 +502,9 @@
<string name="device_settings_allow_off_description">אפשר כבוי כמצב בקרת רעש ניתן לבחירה. כאשר מבוטל, הגבעולים דולגים על כבוי במהלך המחזור.</string>
<string name="device_settings_sleep_detection_label">זיהוי שינה</string>
<string name="device_settings_sleep_detection_description">השהית שמע אוטומטית כשאתה נרדם</string>
<string name="reaction_sleep_channel_label">זיהוי שינה</string>
<string name="reaction_sleep_notification_title">הושהה על ידי זיהוי שינה</string>
<string name="reaction_sleep_notification_text">%1$s דיווח שנרדמת, לכן המוזיקה הושהתה. ניתן להשבית זאת בהגדרות המכשיר.</string>
<string name="device_settings_rename_label">שנה שם</string>
<string name="device_settings_rename_hint">שם המכשיר</string>
<string name="device_settings_rename_confirm">שנה שם</string>
@@ -496,6 +514,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">הגדרות Bluetooth</string>
<string name="device_settings_send_failed">לא ניתן היה להחיל את ההגדרה: %1$s</string>
<string name="device_settings_anc_off_rejected_message">מצב כבוי אינו מופעל במכשיר זה. הפעל את \"אפשר מצב כבוי\" תחת בקרת רעש.</string>
<string name="device_settings_category_battery_label">סוללה</string>
<string name="device_settings_charge_cap_label">מגבלת טעינה מותאמת</string>
<string name="device_settings_charge_cap_description">לומד את השגרה שלך ומשהה את הטעינה סביב 80%% כדי להאריך את חיי הסוללה, ומשלים את הטעינה לפני שסביר שתשתמש בהם.</string>
<string name="device_settings_charge_cap_rejected_message">לא ניתן לשנות את מגבלת הטעינה המותאמת. זוהי תכונה ניסיונית — אנא דווח אם היא ממשיכה להיכשל.</string>
<string name="device_settings_category_connections_label">מכשירים מחוברים</string>
<string name="device_settings_connected_devices_description">מכשירים אחרים המחוברים כעת ל-AirPods אלו</string>
<string name="device_settings_connected_device_label">מכשיר %d</string>
+17 -1
View File
@@ -152,6 +152,7 @@
<string name="widget_config_transparency_label">背景の透明度</string>
<string name="widget_config_show_device_label">デバイス名を表示</string>
<string name="widget_config_reset_label">デフォルトにリセット</string>
<string name="widget_config_preview_resize_hint">ホーム画面に配置した後、ウィジェットのサイズを変更できます。</string>
<string name="widget_config_custom_label">カスタム</string>
<string name="widget_config_preset_material_you">Material You</string>
<string name="widget_config_preset_dark">ダーク</string>
@@ -248,6 +249,7 @@
<string name="pods_unknown_contact_dev">これは不明なデバイスですが、似たメッセージ形式を使用しています。サポートを追加しましょう。連絡してください :)</string>
<string name="pods_none_label_short">デバイスなし</string>
<string name="pods_charging_label">充電中</string>
<string name="pods_charging_optimized_label">最適化済み</string>
<string name="pods_inear_label">装着中</string>
<string name="pods_microphone_label">マイク</string>
<string name="anc_mode_off">オフ</string>
@@ -402,11 +404,15 @@
<string name="device_settings_info_bt_name_label">Bluetoothデバイスラベル</string>
<string name="device_settings_info_model_label">モデル</string>
<string name="device_settings_info_manufacturer_label">メーカー</string>
<string name="device_settings_info_hardware_label">ハードウェア</string>
<string name="device_settings_info_serial_label">シリアル番号</string>
<string name="device_settings_info_firmware_label">ファームウェア</string>
<string name="device_settings_info_firmware_pending_label">保留中のファームウェア</string>
<string name="device_settings_info_build_label">ビルド</string>
<string name="device_settings_info_left_serial_label">左ポッドのシリアル</string>
<string name="device_settings_info_right_serial_label">右ポッドのシリアル</string>
<string name="device_settings_info_left_bonded_label">左ボンド済み</string>
<string name="device_settings_info_right_bonded_label">右ボンド済み</string>
<string name="device_settings_info_details_label">デバイス詳細</string>
<string name="device_settings_info_details_action">デバイス詳細を表示</string>
<string name="device_settings_info_status_label">状態</string>
@@ -416,8 +422,11 @@
<string name="device_settings_not_connected_description">このデバイスは近くにありますが、このスマートフォンに接続されていません。設定と操作にアクセスするには接続してください。</string>
<string name="device_settings_not_connected_connect_action">接続</string>
<string name="device_settings_not_connected_open_settings_action">Bluetooth設定を開く</string>
<string name="device_settings_not_nearby_label">デバイスが近くにありません</string>
<string name="device_settings_not_nearby_description">設定は、このデバイスが近くにあり、スマートフォンに接続されている場合にのみ利用可能です。</string>
<string name="device_settings_aap_unavailable_label">詳細設定を利用できません</string>
<string name="device_settings_aap_unavailable_description">高度なAirPods設定には、このスマートフォンで利用できないBluetooth機能が必要です。デバイスメーカーからの将来のAndroidアップデートで解決される可能性があります。</string>
<string name="device_settings_aap_unavailable_action">互換性トラッカーを表示</string>
<string name="device_settings_category_sound_label">サウンド</string>
<string name="device_settings_category_controls_label">コントロール</string>
<string name="device_settings_nc_one_airpod_label">1つのポッドでANC</string>
@@ -455,7 +464,7 @@
<string name="device_settings_end_call_mute_mic_option_b_title">シングルプレスでマイクをミュート</string>
<string name="device_settings_end_call_mute_mic_option_b_subtitle">ダブルプレスで通話を終了</string>
<string name="device_settings_open_cd">デバイス設定を開く</string>
<string name="overview_card_missing_paired_device_cd">プロファイルにペアリング済みBluetoothデバイスがありません — タップしてプロファイルを編集</string>
<string name="overview_card_missing_paired_device">このプロファイルにペアリングされたBluetoothデバイスがありません</string>
<!-- New device settings -->
<string name="device_settings_category_general_label">一般</string>
<string name="device_settings_microphone_mode_label">マイク</string>
@@ -478,6 +487,9 @@
<string name="device_settings_allow_off_description">ノイズコントロールの選択能なモードとしてオフを許可します。無効の場合、ステムのサイクル中にオフがスキップされます。</string>
<string name="device_settings_sleep_detection_label">睡眠検知</string>
<string name="device_settings_sleep_detection_description">眠りに就いたときに自動的にオーディオを一時停止</string>
<string name="reaction_sleep_channel_label">睡眠検出</string>
<string name="reaction_sleep_notification_title">睡眠検出によって一時停止されました</string>
<string name="reaction_sleep_notification_text">%1$s が眠りについたことを検出したため、音楽が一時停止されました。デバイス設定で無効にできます。</string>
<string name="device_settings_rename_label">名前を変更</string>
<string name="device_settings_rename_hint">デバイス名</string>
<string name="device_settings_rename_confirm">名前を変更</string>
@@ -487,6 +499,10 @@
<string name="device_settings_rename_system_unavailable_bt_settings_action">Bluetooth設定</string>
<string name="device_settings_send_failed">設定を適用できませんでした: %1$s</string>
<string name="device_settings_anc_off_rejected_message">オフモードはこのデバイスで有効になっていません。「ノイズコントロール」で「オフモードを許可」を有効にしてください。</string>
<string name="device_settings_category_battery_label">バッテリー</string>
<string name="device_settings_charge_cap_label">最適化された充電上限</string>
<string name="device_settings_charge_cap_description">あなたのルーティンを学習し、バッテリー寿命を延ばすために80%%前後で充電を一時停止し、使用する直前にイヤポッドを満充電にします。</string>
<string name="device_settings_charge_cap_rejected_message">最適化された充電上限を変更できませんでした。これは実験的な機能です。引き続き失敗する場合は報告してください。</string>
<string name="device_settings_category_connections_label">接続済みデバイス</string>
<string name="device_settings_connected_devices_description">このAirPodsに現在接続中のその他のデバイス</string>
<string name="device_settings_connected_device_label">デバイス %d</string>

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