mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
227a6b2a7b | ||
|
|
5f661d71a6 | ||
|
|
2804543b9d | ||
|
|
380397321c | ||
|
|
d9c8902d34 | ||
|
|
a1b9abd4cb | ||
|
|
5d30844fa8 | ||
|
|
af1b1b4249 | ||
|
|
2cce9ae56c | ||
|
|
4bab7c61a9 | ||
|
|
81e2bc1dba | ||
|
|
df2783d8a1 | ||
|
|
20a6953b54 | ||
|
|
fdd4f69247 | ||
|
|
5ee777b196 | ||
|
|
dd827af3cd | ||
|
|
948205667b | ||
|
|
dbbfa75429 | ||
|
|
78790f73df | ||
|
|
2950d8b3f9 | ||
|
|
4d3a9bc165 | ||
|
|
7d57845502 | ||
|
|
83ef111599 | ||
|
|
0e6848d886 | ||
|
|
1eeeb63a76 | ||
|
|
a4ba302223 | ||
|
|
8c9094d85a | ||
|
|
4529b107dd | ||
|
|
17663bc759 | ||
|
|
278257998b | ||
|
|
9d41979499 | ||
|
|
985b73754e | ||
|
|
e4e4190523 | ||
|
|
ebfb034fdd | ||
|
|
8a81d354e6 | ||
|
|
ac0c4693dc | ||
|
|
4fd7a41c53 | ||
|
|
cc1caf47f0 | ||
|
|
8b9f953818 |
Binary file not shown.
|
Before Width: | Height: | Size: 332 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 760 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 761 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 605 KiB |
@@ -25,16 +25,23 @@ ScreenshotContent.kt (mock data + composables)
|
||||
| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script |
|
||||
| `fastlane/copy_screenshots.sh` | Copies rendered PNGs into fastlane structure |
|
||||
|
||||
## Commit policy
|
||||
|
||||
Only the 6 smoke locales (en-US, de-DE, ja-JP, ar, zh-CN, pt-BR) have `phoneScreenshots/*.png` checked into the repo. Non-smoke locales are excluded by `.gitignore`. This mirrors permission-pilot and keeps repo size small (~7 MB vs ~67 MB for the full 68 locales).
|
||||
|
||||
Play Store's `supply` only uploads what's present in `fastlane/metadata/android/<locale>/images/phoneScreenshots/`. For locales not in the upload, Play Store retains whatever was last pushed. So full localization on Play Store is maintained by an **occasional manual** full regen + `:screenshots_only` upload — not by every PR.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Full run — 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
|
||||
# Default — smoke set (6 locales × 7 screens, ~42 PNGs, single batch).
|
||||
# Use this for local iteration and PRs that touch screenshot content.
|
||||
./fastlane/generate_screenshots.sh --smoke
|
||||
|
||||
# Full run — all 68 locales. Use only when intending to upload to Play Store
|
||||
# (the non-smoke output is .gitignored and should not be committed).
|
||||
./fastlane/generate_screenshots.sh
|
||||
|
||||
# Copy into fastlane directories (run after generate)
|
||||
./fastlane/copy_screenshots.sh
|
||||
|
||||
@@ -48,23 +55,36 @@ ScreenshotContent.kt (mock data + composables)
|
||||
2. Add a `@PreviewTest` function in `PlayStoreScreenshots.kt` that calls it
|
||||
3. Add the function name → filename mapping in `copy_screenshots.sh` `SCREEN_MAP`
|
||||
4. Update the expected count in `generate_screenshots.sh` (composables per locale)
|
||||
5. Run the full pipeline: `generate_screenshots.sh` then `copy_screenshots.sh`
|
||||
5. Run the smoke pipeline: `generate_screenshots.sh --smoke` then `copy_screenshots.sh --clean`
|
||||
|
||||
## Removing or Renaming a Screenshot
|
||||
|
||||
1. Remove the `@PreviewTest` entry and its `SCREEN_MAP` mapping
|
||||
2. Run `generate_screenshots.sh`
|
||||
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
|
||||
2. Run `generate_screenshots.sh --smoke`
|
||||
3. Run `copy_screenshots.sh --clean` — **`--clean` is required** here; without it, old files (e.g. a renamed `8_reaction_settings.png`) stay in `fastlane/metadata/android/<smoke locale>/images/phoneScreenshots/` and get uploaded to Play Store
|
||||
|
||||
## After UI Changes
|
||||
|
||||
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate:
|
||||
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate the smoke set:
|
||||
|
||||
```bash
|
||||
./fastlane/generate_screenshots.sh
|
||||
./fastlane/generate_screenshots.sh --smoke
|
||||
./fastlane/copy_screenshots.sh --clean
|
||||
```
|
||||
|
||||
## Refreshing all locales on Play Store
|
||||
|
||||
Periodic, manual operation — not per-PR:
|
||||
|
||||
```bash
|
||||
./fastlane/generate_screenshots.sh # full, ~30 min, 477 PNGs
|
||||
./fastlane/copy_screenshots.sh --clean
|
||||
bundle exec fastlane screenshots_only # uploads all 68 locales to Play Store
|
||||
git checkout -- fastlane/metadata/android/ # discard non-smoke changes (gitignored anyway)
|
||||
```
|
||||
|
||||
The `.gitignore` rule keeps non-smoke output unstaged automatically, so only the smoke locales' refreshed PNGs would show up as modifications and can be committed.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Batch size defaults to 2 locales; renders per batch = `BATCH_SIZE × screen count` (currently 2 × 7 = 14). Small batches avoid layoutlib memory leaks (~10MB/image)
|
||||
|
||||
+10
@@ -17,6 +17,16 @@
|
||||
.kotlin
|
||||
# Screenshot test reference images (ephemeral, regenerated on demand)
|
||||
app/src/screenshotTest*/reference/
|
||||
# Play Store phone screenshots: commit only the 6 smoke locales (mirrors permission-pilot).
|
||||
# Full localization is uploaded by occasional manual regen + screenshots_only;
|
||||
# Play Store retains previously-uploaded screenshots for locales not pushed.
|
||||
fastlane/metadata/android/*/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/en-US/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/de-DE/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/ja-JP/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/ar/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/zh-CN/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/pt-BR/images/phoneScreenshots/*.png
|
||||
.codex
|
||||
protocol-research/
|
||||
_site/
|
||||
|
||||
@@ -81,8 +81,7 @@ Currently supported models:
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
|
||||
<img src="https://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
|
||||
<img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1_dashboard_light.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2_dashboard_dark.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/3_case_popup.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/4_widget_configuration.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5_device_profiles.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/6_add_profile.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/7_device_settings_reactions.png" width="100">
|
||||
|
||||
## Thanks to
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -130,6 +130,7 @@ android {
|
||||
//noinspection WrongGradleMethod
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import eu.darken.capod.common.compose.preview.MOCK_NOW
|
||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||
@@ -37,17 +38,26 @@ import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard
|
||||
internal const val DS = "spec:width=1080px,height=2400px,dpi=428"
|
||||
|
||||
@Composable
|
||||
internal fun DashboardContent() = PreviewWrapper {
|
||||
internal fun DashboardContent(showAap: Boolean = false) = PreviewWrapper {
|
||||
val devices = if (showAap) {
|
||||
listOf(
|
||||
MockPodDataProvider.dualPodMonitoredWithAap(),
|
||||
MockPodDataProvider.singlePodMonitoredWithAap(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
MockPodDataProvider.dualPodMonitoredMixed(),
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
)
|
||||
}
|
||||
OverviewScreen(
|
||||
state = OverviewViewModel.State(
|
||||
now = MOCK_NOW,
|
||||
permissions = emptySet(),
|
||||
devices = listOf(
|
||||
MockPodDataProvider.dualPodMonitoredMixed(),
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
),
|
||||
isDebugMode = false,
|
||||
devices = devices,
|
||||
isDebug = false,
|
||||
isBluetoothEnabled = true,
|
||||
profiles = listOf(
|
||||
MockPodDataProvider.profile("My AirPods Pro", PodModel.AIRPODS_PRO2),
|
||||
@@ -83,19 +93,38 @@ internal fun DeviceProfilesContent() = PreviewWrapper {
|
||||
|
||||
@Composable
|
||||
internal fun AddProfileContent() = PreviewWrapper {
|
||||
val pairedAirPods = BluetoothDevice2(
|
||||
address = "AA:BB:CC:DD:EE:FF",
|
||||
name = "AirPods Pro",
|
||||
seenFirstAt = MOCK_NOW,
|
||||
)
|
||||
val bondedItems = listOf(
|
||||
DeviceProfileCreationViewModel.BondedDeviceItem(
|
||||
device = pairedAirPods,
|
||||
claimedByProfile = null,
|
||||
),
|
||||
DeviceProfileCreationViewModel.BondedDeviceItem(
|
||||
device = BluetoothDevice2(
|
||||
address = "11:22:33:44:55:66",
|
||||
name = "Living Room TV",
|
||||
seenFirstAt = MOCK_NOW,
|
||||
),
|
||||
claimedByProfile = null,
|
||||
),
|
||||
)
|
||||
DeviceProfileCreationScreen(
|
||||
state = DeviceProfileCreationViewModel.State(
|
||||
isEditMode = false,
|
||||
name = "",
|
||||
name = "My AirPods Pro",
|
||||
nameError = null,
|
||||
selectedModel = null,
|
||||
selectedModel = PodModel.AIRPODS_PRO2,
|
||||
availableModels = PodModel.entries.filter { it != PodModel.UNKNOWN },
|
||||
identityKey = null,
|
||||
encryptionKey = null,
|
||||
selectedDevice = null,
|
||||
bondedDeviceItems = emptyList(),
|
||||
selectedDevice = pairedAirPods,
|
||||
bondedDeviceItems = bondedItems,
|
||||
minimumSignalQuality = 0.15f,
|
||||
canSave = false,
|
||||
canSave = true,
|
||||
),
|
||||
onBack = {},
|
||||
onSave = {},
|
||||
@@ -212,7 +241,7 @@ private fun PreviewDashboardLight() = DashboardContent()
|
||||
|
||||
@Preview(name = "2 - Dashboard Dark", locale = "en", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true)
|
||||
@Composable
|
||||
private fun PreviewDashboardDark() = DashboardContent()
|
||||
private fun PreviewDashboardDark() = DashboardContent(showAap = true)
|
||||
|
||||
@Preview(name = "3 - Case Pop-up", device = DS, showSystemUi = true)
|
||||
@Composable
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">تبرُّع</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">أنا متبرع بالفعل</string>
|
||||
<string name="foss_upgrade_no_money_label">أُنفق كل أموالي على AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS مجاني ومفتوح المصدر. إذا وجدته مفيداً، فكر في دعم التطوير للمساعدة في استمرار المشروع.</string>
|
||||
<string name="upgrade_foss_sponsor_action">ادعم التطوير</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبع. لا قيود على Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">عدت بسرعة؟ دعمك يبقي CAPod حياً.</string>
|
||||
<string name="foss_upgrade_donate_label">تبرع</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">أنا متبرّع فعلًا</string>
|
||||
<string name="foss_upgrade_no_money_label">أنفق كل أموالي على AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS مجاني ومفتوح المصدر. إذا وجدته مفيدًا، ففكّر في دعم تطويره للمساعدة في استمرار المشروع.</string>
|
||||
<string name="upgrade_foss_sponsor_action">دعم المشروع</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبع. لا قيود من Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">هل عدت فعلًا؟ دعمكم يُبقي CAPod حيًا.</string>
|
||||
<string name="upgrade_badge_label">البرمجيات الحرة (FOSS)</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">خدمات Google Play غير متاحة.</string>
|
||||
<string name="upgrades_gplay_unavailable_error">خدمات Google Play غير متوفّرة.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">لم يتم العثور على أي مشتريات. هل تستخدم الحساب الصحيح؟</string>
|
||||
<string name="upgrades_gplay_billing_error_label">خطأ في Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">حدث خطأ في Google Play. يرجى المحاولة مرة أخرى لاحقًا أو إعادة تشغيل هاتفك.\n\nخطأ: %s</string>
|
||||
<string name="upgrades_gplay_billing_error_description">حدث خطأ في Google Play. يُرجى المحاولة لاحقًا أو إعادة تشغيل هاتفك.\n\nخطأ: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">خطأ في فوترة Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">حدث خطأ عند طلب تفاصيل الشراء من Google Play. امسح ذاكرة التخزين المؤقت لـ Google Play وأعد تشغيل هاتفك.\n\nخطأ %s</string>
|
||||
<string name="upgrade_restore_action">استعادة المُشتَرَى</string>
|
||||
<string name="upgrade_badge_label">احترافي</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">حدث خطأ أثناء طلب تفاصيل عملية الشراء من Google Play. يُرجى مسح ذاكرة التخزين المؤقّت لـ Google Play وإعادة تشغيل هاتفك.\n\nخطأ %s</string>
|
||||
<string name="upgrade_restore_action">استعادة المشتريات</string>
|
||||
<string name="upgrade_badge_label">النسخة الاحترافية</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package eu.darken.capod.common
|
||||
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioPlaybackConfiguration
|
||||
import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -14,48 +17,103 @@ class MediaControl @Inject constructor(
|
||||
private val audioManager: AudioManager,
|
||||
private val timeSource: TimeSource,
|
||||
) {
|
||||
private var capPauseExpiryElapsedRealtime: Long = 0L
|
||||
/**
|
||||
* Set when [sendPause] dispatches a pause we expect to take effect, cleared when [sendPlay]
|
||||
* dispatches a resume or when music transitions inactive→active from any source. Read by
|
||||
* [PlayPause] to gate auto-resume on pod-in: only resume if we're the ones who paused.
|
||||
*
|
||||
* "Sticky" — no time-based expiry. The original 15-second window was too short for typical
|
||||
* pod-out conversations and had a known race where music starting again via another source
|
||||
* (e.g. user manually resumed) could trigger a stray play-key dispatch on the next pod-in.
|
||||
*/
|
||||
@Volatile private var capPaused: Boolean = false
|
||||
@Volatile private var lastKnownMusicActive: Boolean = false
|
||||
|
||||
private val playbackCallback = object : AudioManager.AudioPlaybackCallback() {
|
||||
override fun onPlaybackConfigChanged(configs: List<AudioPlaybackConfiguration>) {
|
||||
val nowActive = audioManager.isMusicActive
|
||||
if (!lastKnownMusicActive && nowActive) {
|
||||
// Music started by some source (could be us via sendPlay or someone else).
|
||||
// Either way, our pause memory is stale — drop it so a future pod-in doesn't
|
||||
// re-fire a play key over already-active music.
|
||||
capPaused = false
|
||||
}
|
||||
lastKnownMusicActive = nowActive
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Seed the active flag from current state so we won't miss the next inactive→active
|
||||
// transition if music is already playing when MediaControl is constructed.
|
||||
lastKnownMusicActive = audioManager.isMusicActive
|
||||
audioManager.registerAudioPlaybackCallback(playbackCallback, null)
|
||||
}
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = audioManager.isMusicActive
|
||||
|
||||
val wasRecentlyPausedByCap: Boolean
|
||||
get() = capPauseExpiryElapsedRealtime > timeSource.elapsedRealtime()
|
||||
get() = capPaused
|
||||
|
||||
suspend fun sendPlay() {
|
||||
log(TAG, INFO) { "sendPlay()" }
|
||||
if (audioManager.isMusicActive && !wasRecentlyPausedByCap) {
|
||||
if (audioManager.isMusicActive && !capPaused) {
|
||||
log(TAG, INFO) { "Music is already playing, not sending play" }
|
||||
return
|
||||
}
|
||||
sendKey(KeyEvent.KEYCODE_MEDIA_PLAY)
|
||||
clearRecentCapPause()
|
||||
capPaused = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param rememberForResume When `true`, arms [wasRecentlyPausedByCap] so a subsequent
|
||||
* pod-in transition can auto-resume — set this only from the auto-pause / ear-detection
|
||||
* flow. When `false` (default), the dispatched pause clears any pending auto-resume —
|
||||
* this is the path for stem-press play/pause, sleep detection, and anywhere else CAPod
|
||||
* is relaying an explicit user choice to stop playback. Matches Apple's iOS/macOS
|
||||
* behavior where only ear-removal auto-pauses are eligible for auto-resume.
|
||||
*
|
||||
* Returns `true` when a key event was actually dispatched, `false` when the call was a
|
||||
* no-op because nothing was playing. A no-op leaves the existing flag state untouched
|
||||
* (so a sleep-reaction firing while music is already paused doesn't accidentally cancel
|
||||
* a pending auto-resume from a recent ear-removal pause). 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()" }
|
||||
suspend fun sendPause(rememberForResume: Boolean = false): Boolean {
|
||||
log(TAG, INFO) { "sendPause(rememberForResume=$rememberForResume)" }
|
||||
if (!audioManager.isMusicActive) {
|
||||
log(TAG, INFO) { "Music is not playing, not sending pause" }
|
||||
return false
|
||||
}
|
||||
// Set BEFORE the suspending sendKey() call. If we set after, an inactive→active
|
||||
// playback callback that fires during the dispatch (e.g. a fast user resume on the
|
||||
// phone, or another app grabbing audio focus and immediately starting) could clear
|
||||
// capPaused mid-dispatch and we'd then overwrite it back to true on a stale pause.
|
||||
// This single explicit assignment also covers the contract that an explicit user
|
||||
// pause cancels a pending auto-resume.
|
||||
capPaused = rememberForResume
|
||||
sendKey(KeyEvent.KEYCODE_MEDIA_PAUSE)
|
||||
markRecentCapPause()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches MEDIA_STOP and clears any pending auto-resume — Stop is an explicit user
|
||||
* "stay stopped" action, so a later pod-in must not auto-resume from a prior auto-pause.
|
||||
*/
|
||||
suspend fun sendStop() {
|
||||
log(TAG, INFO) { "sendStop()" }
|
||||
capPaused = false
|
||||
sendKey(KeyEvent.KEYCODE_MEDIA_STOP)
|
||||
}
|
||||
|
||||
suspend fun sendPlayPause() {
|
||||
log(TAG) { "sendPlayPause()" }
|
||||
if (wasRecentlyPausedByCap) {
|
||||
if (capPaused) {
|
||||
sendPlay()
|
||||
return
|
||||
}
|
||||
@@ -101,16 +159,71 @@ class MediaControl @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun markRecentCapPause() {
|
||||
capPauseExpiryElapsedRealtime = timeSource.elapsedRealtime() + RECENT_CAP_PAUSE_WINDOW_MS
|
||||
/** Current STREAM_MUSIC volume index. Used to detect user-initiated volume changes after a duck. */
|
||||
fun currentMusicVolume(): Int = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
|
||||
/**
|
||||
* Lowers STREAM_MUSIC volume by [reductionPercent] (relative to the current level) and returns
|
||||
* the prior + the volume actually applied, so the caller can later restore it and detect whether
|
||||
* the user changed the volume in the meantime.
|
||||
*
|
||||
* Returns `null` (no-op) when nothing is playing, the device has fixed volume, or the computed
|
||||
* target wouldn't actually lower the volume. No [AudioManager.FLAG_SHOW_UI] — this fires on a
|
||||
* frequent push event and the volume panel flashing would be noisy. The applied target is read
|
||||
* back from the system because Bluetooth absolute-volume routes can quantize the requested value.
|
||||
*/
|
||||
fun duckMusicVolume(reductionPercent: Int): VolumeDuck? {
|
||||
if (!audioManager.isMusicActive) {
|
||||
log(TAG, INFO) { "duckMusicVolume: nothing playing, skipping" }
|
||||
return null
|
||||
}
|
||||
if (audioManager.isVolumeFixed) {
|
||||
log(TAG, INFO) { "duckMusicVolume: device has fixed volume, skipping" }
|
||||
return null
|
||||
}
|
||||
val percent = reductionPercent.coerceIn(0, 100)
|
||||
val max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
val min = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val prior = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
val target = (prior * (100 - percent) / 100).coerceIn(min, max)
|
||||
if (target >= prior) {
|
||||
log(TAG, INFO) { "duckMusicVolume: target $target >= current $prior, skipping" }
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, target, 0)
|
||||
val applied = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
log(TAG, INFO) { "duckMusicVolume($percent%): $prior -> $applied (requested $target)" }
|
||||
VolumeDuck(priorVolume = prior, appliedVolume = applied)
|
||||
} catch (e: SecurityException) {
|
||||
// setStreamVolume throws under Do-Not-Disturb without notification policy access.
|
||||
log(TAG, WARN) { "duckMusicVolume: setStreamVolume denied: ${e.message}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearRecentCapPause() {
|
||||
capPauseExpiryElapsedRealtime = 0L
|
||||
/** Restores STREAM_MUSIC to [priorVolume]. No-op on fixed-volume devices; denials are logged. */
|
||||
fun restoreMusicVolume(priorVolume: Int) {
|
||||
if (audioManager.isVolumeFixed) return
|
||||
try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, priorVolume, 0)
|
||||
log(TAG, INFO) { "restoreMusicVolume($priorVolume)" }
|
||||
} catch (e: SecurityException) {
|
||||
log(TAG, WARN) { "restoreMusicVolume: setStreamVolume denied: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
/** Snapshot of a volume duck so the caller can restore the prior level and detect user changes. */
|
||||
data class VolumeDuck(
|
||||
val priorVolume: Int,
|
||||
val appliedVolume: Int,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("MediaControl")
|
||||
private const val RECENT_CAP_PAUSE_WINDOW_MS = 15_000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import javax.inject.Singleton
|
||||
class BleScanner @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val fakeBleData: FakeBleData,
|
||||
private val scanResultForwarder: BleScanResultForwarder,
|
||||
private val timeSource: TimeSource,
|
||||
) {
|
||||
@@ -206,7 +205,6 @@ class BleScanner @Inject constructor(
|
||||
log(TAG) { "BleScanner stopped" }
|
||||
}
|
||||
}
|
||||
.map { fakeBleData.maybeAddfakeData(it) }
|
||||
|
||||
private val receiverIntent by lazy {
|
||||
Intent(context, BleScanResultReceiver::class.java).apply {
|
||||
|
||||
@@ -4,12 +4,8 @@ import android.bluetooth.BluetoothDevice
|
||||
import java.time.Instant
|
||||
|
||||
data class BluetoothDevice2(
|
||||
internal val internal: BluetoothDevice,
|
||||
val address: BluetoothAddress,
|
||||
val name: String?,
|
||||
val seenFirstAt: Instant,
|
||||
) {
|
||||
val address: BluetoothAddress
|
||||
get() = internal.address
|
||||
|
||||
val name: String?
|
||||
get() = internal.name
|
||||
}
|
||||
internal val internal: BluetoothDevice? = null,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -23,6 +24,7 @@ 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.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.pods.core.apple.ble.protocol.ContinuityProtocol
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
@@ -255,6 +257,8 @@ class BluetoothManager2 @Inject constructor(
|
||||
}
|
||||
.map { device ->
|
||||
BluetoothDevice2(
|
||||
address = device.address,
|
||||
name = device.name,
|
||||
internal = device,
|
||||
seenFirstAt = seenDevicesLock.withLock {
|
||||
seenDevicesCache[device.address] ?: run {
|
||||
@@ -304,6 +308,8 @@ class BluetoothManager2 @Inject constructor(
|
||||
val wrappedDevices = rawDevices.map { device ->
|
||||
|
||||
BluetoothDevice2(
|
||||
address = device.address,
|
||||
name = device.name,
|
||||
internal = device,
|
||||
seenFirstAt = seenDevicesLock.withLock {
|
||||
seenDevicesCache[device.address] ?: run {
|
||||
@@ -386,9 +392,6 @@ class BluetoothManager2 @Inject constructor(
|
||||
initialValue = emptySet(),
|
||||
)
|
||||
|
||||
private var _isNudgeAvailable: Boolean = true
|
||||
val isNudgeAvailable: Boolean get() = _isNudgeAvailable
|
||||
|
||||
/**
|
||||
* Set the Android-local alias for a bonded [device]. Updates what Android's system Bluetooth
|
||||
* settings display without touching the AirPods firmware itself. Uses reflection on the hidden
|
||||
@@ -408,9 +411,10 @@ class BluetoothManager2 @Inject constructor(
|
||||
@android.annotation.SuppressLint("MissingPermission")
|
||||
fun setDeviceAlias(device: BluetoothDevice2, alias: String): Boolean {
|
||||
return try {
|
||||
val target = device.internal ?: return false
|
||||
val method = BluetoothDevice::class.java.getDeclaredMethod("setAlias", String::class.java)
|
||||
.apply { isAccessible = true }
|
||||
val result = method.invoke(device.internal, alias) as? Boolean ?: false
|
||||
val result = method.invoke(target, alias) as? Boolean ?: false
|
||||
log(TAG) { "setDeviceAlias(${device.address}, $alias) -> $result" }
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
@@ -420,28 +424,42 @@ class BluetoothManager2 @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun nudgeConnection(device: BluetoothDevice2): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
||||
try {
|
||||
log(TAG) { "Nudging Android connection to $device" }
|
||||
suspend fun nudgeConnection(device: BluetoothDevice2): NudgeAttemptResult =
|
||||
getBluetoothProfile().map { bluetoothProfile ->
|
||||
try {
|
||||
log(TAG) { "Nudging Android connection to $device" }
|
||||
|
||||
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
|
||||
"connect", BluetoothDevice::class.java
|
||||
).apply { isAccessible = true }
|
||||
val target = device.internal ?: return@map NudgeAttemptResult.Rejected
|
||||
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
|
||||
"connect", BluetoothDevice::class.java
|
||||
).apply { isAccessible = true }
|
||||
|
||||
val accepted = connectMethod.invoke(bluetoothProfile.proxy, device.internal) as? Boolean ?: false
|
||||
log(TAG) { "Nudged connection to $device — accepted=$accepted" }
|
||||
accepted
|
||||
} catch (e: Exception) {
|
||||
val isSecurityException = e is SecurityException ||
|
||||
(e is java.lang.reflect.InvocationTargetException && e.cause is SecurityException)
|
||||
if (isSecurityException) {
|
||||
log(TAG, ERROR) { "nudgeConnection is permanently unavailable: missing MODIFY_PHONE_STATE permission" }
|
||||
_isNudgeAvailable = false
|
||||
val accepted = connectMethod.invoke(bluetoothProfile.proxy, target) as? Boolean ?: false
|
||||
log(TAG) { "Nudged connection to $device — accepted=$accepted" }
|
||||
if (accepted) NudgeAttemptResult.Accepted else NudgeAttemptResult.Rejected
|
||||
} catch (e: Exception) {
|
||||
val isSecurityException = e is SecurityException ||
|
||||
(e is java.lang.reflect.InvocationTargetException && e.cause is SecurityException)
|
||||
if (!isSecurityException) {
|
||||
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
|
||||
return@map NudgeAttemptResult.Rejected
|
||||
}
|
||||
// BLUETOOTH_CONNECT is only a runtime permission on Android 12+. On older
|
||||
// versions a SecurityException can't be a missing-permission failure for that
|
||||
// permission — it's hidden-API enforcement (e.g. MODIFY_PHONE_STATE) and we
|
||||
// should persist BROKEN.
|
||||
val missingBtConnect = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
!Permission.BLUETOOTH_CONNECT.isGranted(context)
|
||||
if (missingBtConnect) {
|
||||
log(TAG, WARN) { "nudgeConnection failed because BLUETOOTH_CONNECT is not granted" }
|
||||
NudgeAttemptResult.UnavailableMissingPermission
|
||||
} else {
|
||||
log(TAG, ERROR) { "nudgeConnection is permanently unavailable on this device: $e" }
|
||||
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
|
||||
NudgeAttemptResult.UnavailableHiddenApi
|
||||
}
|
||||
}
|
||||
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
|
||||
false
|
||||
}
|
||||
}.first()
|
||||
}.first()
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Bluetooth", "Manager2")
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package eu.darken.capod.common.bluetooth
|
||||
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.fromHex
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@Reusable
|
||||
class FakeBleData @Inject constructor(
|
||||
private val debugSettings: DebugSettings,
|
||||
private val timeSource: TimeSource,
|
||||
) {
|
||||
|
||||
fun maybeAddfakeData(originals: Collection<BleScanResult>): Collection<BleScanResult> {
|
||||
if (!debugSettings.showFakeData.valueBlocking) return originals
|
||||
return originals + getFakeData()
|
||||
}
|
||||
|
||||
fun getFakeData(): Collection<BleScanResult> {
|
||||
val fakeDevices = mutableListOf<BleScanResult>()
|
||||
val now = timeSource.now()
|
||||
val generatedAt = timeSource.elapsedRealtimeNanos()
|
||||
// AirPods Gen1
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "78:73:AF:B4:85:22",
|
||||
rssi = Random.nextInt(100) * -1,
|
||||
generatedAtNanos = generatedAt + 100,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// AirPods Gen2
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "78:73:FF:B4:85:5E",
|
||||
rssi = Random.nextInt(100) * -1,
|
||||
generatedAtNanos = generatedAt + 100,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// AirPods Gen3
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "4E:9E:D1:49:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = generatedAt + 200,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
// AirPods Max
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "7E:E5:C7:65:D2:B5",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = generatedAt + 300,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
// BeatsFlex
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "5E:9E:D1:49:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = generatedAt + 400,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// Tws i99999
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "5E:9E:D1:29:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = generatedAt + 400,
|
||||
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
// Unknown Device
|
||||
BleScanResult(
|
||||
receivedAt = now,
|
||||
address = "6E:9E:D1:49:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = generatedAt + 500,
|
||||
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".fromHex())
|
||||
).run {
|
||||
fakeDevices.add(this)
|
||||
}
|
||||
|
||||
return fakeDevices
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package eu.darken.capod.common.bluetooth
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class NudgeAvailability {
|
||||
@SerialName("nudge.unknown") UNKNOWN,
|
||||
@SerialName("nudge.available") AVAILABLE,
|
||||
@SerialName("nudge.broken") BROKEN,
|
||||
}
|
||||
|
||||
enum class NudgeAttemptResult {
|
||||
Accepted,
|
||||
Rejected,
|
||||
UnavailableHiddenApi,
|
||||
UnavailableMissingPermission,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.darken.capod.common.bluetooth
|
||||
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.datastore.value
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
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.main.core.GeneralSettings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NudgeCapabilityStore @Inject constructor(
|
||||
generalSettings: GeneralSettings,
|
||||
@AppScope private val appScope: CoroutineScope,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
) {
|
||||
|
||||
private val persistedValue = generalSettings.nudgeAvailability
|
||||
|
||||
// Resolve the persisted value synchronously at construction so consumers that read
|
||||
// `availability.value` (resolver, AutoConnect precondition, UI state) never see the
|
||||
// synthetic UNKNOWN seed before the first DataStore emission arrives. On a known-broken
|
||||
// device this prevents one bonus ALWAYS-mode + nudge-attempt cycle per cold start.
|
||||
val availability: StateFlow<NudgeAvailability> = persistedValue.flow
|
||||
.stateIn(
|
||||
scope = appScope + dispatcherProvider.IO,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = persistedValue.valueBlocking,
|
||||
)
|
||||
|
||||
fun record(result: NudgeAttemptResult) {
|
||||
val verdict = verdictFor(result) ?: return
|
||||
if (availability.value == verdict) return
|
||||
log(TAG, INFO) { "Recording nudge verdict: $verdict (from $result)" }
|
||||
appScope.launch(dispatcherProvider.IO) {
|
||||
persistedValue.value(verdict)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Bluetooth", "NudgeCapabilityStore")
|
||||
|
||||
internal fun verdictFor(result: NudgeAttemptResult): NudgeAvailability? = when (result) {
|
||||
NudgeAttemptResult.Accepted -> NudgeAvailability.AVAILABLE
|
||||
NudgeAttemptResult.UnavailableHiddenApi -> NudgeAvailability.BROKEN
|
||||
NudgeAttemptResult.Rejected,
|
||||
NudgeAttemptResult.UnavailableMissingPermission -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,18 +180,21 @@ object MockPodDataProvider {
|
||||
|
||||
fun dualPodMonitoredMixed(): PodDevice = PodDevice(
|
||||
profileId = "preview-dual-mixed",
|
||||
label = "My AirPods Pro",
|
||||
ble = airPodsProMixed(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun dualPodMonitoredWithKeys(): PodDevice = PodDevice(
|
||||
profileId = "preview-dual-keys",
|
||||
label = "My AirPods Pro",
|
||||
ble = airPodsProWithKeys(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun dualPodMonitoredWithAap(): PodDevice = PodDevice(
|
||||
profileId = "preview-dual-aap",
|
||||
label = "My AirPods Pro",
|
||||
ble = airPodsProWithKeys(),
|
||||
aap = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
@@ -227,12 +230,14 @@ object MockPodDataProvider {
|
||||
|
||||
fun singlePodMonitored(): PodDevice = PodDevice(
|
||||
profileId = "preview-single",
|
||||
label = "AirPods Max",
|
||||
ble = airPodsMax(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun singlePodMonitoredWithAap(): PodDevice = PodDevice(
|
||||
profileId = "preview-single-aap",
|
||||
label = "AirPods Max",
|
||||
ble = airPodsMaxCharging(),
|
||||
aap = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -23,9 +24,10 @@ class DataStoreValue<T>(
|
||||
) {
|
||||
val keyName: String get() = key.name
|
||||
|
||||
val flow: Flow<T> = dataStore.data.map { prefs ->
|
||||
reader(prefs[key])
|
||||
}
|
||||
val flow: Flow<T> = dataStore.data
|
||||
.map { prefs -> prefs[key] }
|
||||
.distinctUntilChanged()
|
||||
.map { raw -> reader(raw) }
|
||||
|
||||
data class Updated<T>(val old: T, val new: T)
|
||||
|
||||
|
||||
@@ -8,9 +8,19 @@ import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.asLogSummary
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
object Bugs {
|
||||
var reporter: AutomaticBugReporter? = null
|
||||
|
||||
/**
|
||||
* True while the debug log recorder is actively recording. Surfaces extra
|
||||
* diagnostic UI (e.g. raw BLE payload hex on device cards) so users helping
|
||||
* to add support for new headphones can capture useful data without a
|
||||
* permanent debug toggle.
|
||||
*/
|
||||
val isDebug = MutableStateFlow(false)
|
||||
|
||||
fun report(
|
||||
tag: String,
|
||||
message: String,
|
||||
|
||||
@@ -28,10 +28,4 @@ class DebugSettings @Inject constructor(
|
||||
// Reporting is opt-out for gplay, and opt-in for github builds
|
||||
defaultValue = BuildConfigWrap.FLAVOR == BuildConfigWrap.Flavor.GPLAY
|
||||
)
|
||||
val isDebugModeEnabled = dataStore.createValue("debug.mode.enabled", false)
|
||||
|
||||
val showFakeData = dataStore.createValue("debug.fakedata.enabled", false)
|
||||
|
||||
val showUnfiltered = dataStore.createValue("debug.blescanner.unfiltered.enabled", false)
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import eu.darken.capod.common.SystemTimeSource
|
||||
import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
@@ -126,6 +127,10 @@ class RecorderModule @Inject constructor(
|
||||
}
|
||||
}
|
||||
.launchIn(appScope)
|
||||
|
||||
internalState.flow
|
||||
.onEach { Bugs.isDebug.value = it.isRecording }
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
private fun createSessionDir(): File {
|
||||
|
||||
@@ -36,9 +36,6 @@ object Nav {
|
||||
@Serializable
|
||||
data object General : Settings
|
||||
|
||||
@Serializable
|
||||
data object Debug : Settings
|
||||
|
||||
@Serializable
|
||||
data object Support : Settings
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
import eu.darken.capod.common.bluetooth.NudgeAvailability
|
||||
import eu.darken.capod.common.datastore.createValue
|
||||
import eu.darken.capod.common.serialization.ByteArrayBase64Serializer
|
||||
import eu.darken.capod.common.serialization.SerializationCapod
|
||||
@@ -36,8 +37,6 @@ class GeneralSettings @Inject constructor(
|
||||
|
||||
private val dataStore: DataStore<Preferences> get() = context.dataStore
|
||||
|
||||
val monitorMode = dataStore.createValue("core.monitor.mode", MonitorMode.AUTOMATIC, json, onErrorFallbackToDefault = true)
|
||||
|
||||
val useExtraMonitorNotification = dataStore.createValue("core.monitor.notification.connected", false)
|
||||
|
||||
val keepConnectedNotificationAfterDisconnect =
|
||||
@@ -74,10 +73,18 @@ class GeneralSettings @Inject constructor(
|
||||
val isOffloadedBatchingDisabled = dataStore.createValue("core.compat.offloaded.batching.disabled", false)
|
||||
val useIndirectScanResultCallback = dataStore.createValue("core.compat.indirectcallback.enabled", false)
|
||||
|
||||
/** Runtime-detected verdict for whether `BluetoothHeadset.connect()` reflection works on this device. */
|
||||
val nudgeAvailability = dataStore.createValue(
|
||||
"core.bluetooth.nudge.availability", NudgeAvailability.UNKNOWN, json,
|
||||
onErrorFallbackToDefault = true,
|
||||
)
|
||||
|
||||
val isOnboardingDone = dataStore.createValue("core.onboarding.done", false)
|
||||
|
||||
val reactionsHintDismissed = dataStore.createValue("ui.hint.reactions_per_device.dismissed", false)
|
||||
|
||||
val hideUnmatchedDevices = dataStore.createValue("ui.overview.unmatched.hidden", false)
|
||||
|
||||
val themeMode = dataStore.createValue(
|
||||
"core.ui.theme.mode", ThemeMode.SYSTEM, json,
|
||||
onErrorFallbackToDefault = BuildConfigWrap.BUILD_TYPE != BuildConfigWrap.BuildType.DEV,
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package eu.darken.capod.main.core
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import eu.darken.capod.R
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class MonitorMode(
|
||||
@StringRes val labelRes: Int
|
||||
val intensity: Int,
|
||||
) {
|
||||
@SerialName("monitor.mode.manual") MANUAL(
|
||||
R.string.settings_monitor_mode_manual_label
|
||||
),
|
||||
@SerialName("monitor.mode.automatic") AUTOMATIC(
|
||||
R.string.settings_monitor_mode_automatic_label
|
||||
),
|
||||
@SerialName("monitor.mode.always") ALWAYS(
|
||||
R.string.settings_monitor_mode_always_label
|
||||
),
|
||||
}
|
||||
@SerialName("monitor.mode.manual") MANUAL(intensity = 0),
|
||||
@SerialName("monitor.mode.automatic") AUTOMATIC(intensity = 1),
|
||||
@SerialName("monitor.mode.always") ALWAYS(intensity = 2),
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.common.permissions.isRequired
|
||||
import eu.darken.capod.monitor.core.MonitorModeResolver
|
||||
import eu.darken.capod.profiles.core.toReactionConfig
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -21,7 +22,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class PermissionTool @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val monitorModeResolver: MonitorModeResolver,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
) {
|
||||
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
||||
@@ -41,13 +42,11 @@ class PermissionTool @Inject constructor(
|
||||
|
||||
val missingPermissions: Flow<Set<Permission>> = combine(
|
||||
permissionCheckTrigger,
|
||||
generalSettings.monitorMode.flow,
|
||||
monitorModeResolver.effectiveMode,
|
||||
anyPopupEnabled,
|
||||
) { _, monitorMode, showPopUp ->
|
||||
Permission.entries
|
||||
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
|
||||
.filter { it != Permission.ACCESS_BACKGROUND_LOCATION || monitorMode == MonitorMode.ALWAYS }
|
||||
.filter { it != Permission.SYSTEM_ALERT_WINDOW || showPopUp }
|
||||
.filter { isApplicable(it, monitorMode, showPopUp) }
|
||||
.filter { it.isRequired(context) }
|
||||
.toSet()
|
||||
}
|
||||
@@ -61,5 +60,24 @@ class PermissionTool @Inject constructor(
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("PermissionTool")
|
||||
|
||||
/**
|
||||
* Whether [permission] is applicable for the user's current configuration.
|
||||
* Three permissions are conditional on monitor mode or popup usage:
|
||||
* - [Permission.IGNORE_BATTERY_OPTIMIZATION] only when always-on scanning is needed
|
||||
* - [Permission.ACCESS_BACKGROUND_LOCATION] same
|
||||
* - [Permission.SYSTEM_ALERT_WINDOW] only when at least one popup reaction is enabled
|
||||
* Everything else is unconditionally applicable.
|
||||
*/
|
||||
internal fun isApplicable(
|
||||
permission: Permission,
|
||||
monitorMode: MonitorMode,
|
||||
anyPopupEnabled: Boolean,
|
||||
): Boolean = when (permission) {
|
||||
Permission.IGNORE_BATTERY_OPTIMIZATION,
|
||||
Permission.ACCESS_BACKGROUND_LOCATION -> monitorMode == MonitorMode.ALWAYS
|
||||
Permission.SYSTEM_ALERT_WINDOW -> anyPopupEnabled
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.SingleEventFlow
|
||||
import eu.darken.capod.common.navigation.LocalNavigationController
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.navigation.NavigationController
|
||||
@@ -50,6 +51,11 @@ class MainActivity : Activity2() {
|
||||
@Inject lateinit var popUpWindow: PopUpWindow
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
|
||||
// Buffers warm-start intents (onNewIntent) so they are consumed from inside the Compose tree,
|
||||
// after navCtrl.setup(backStack) has run. Calling navCtrl directly from onNewIntent races with
|
||||
// the asynchronous Compose lambda and crashes if the back stack is not yet registered.
|
||||
private val warmIntents = SingleEventFlow<Intent>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
@@ -80,7 +86,11 @@ class MainActivity : Activity2() {
|
||||
navCtrl.setup(backStack)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Cold-start intent (the activity's launching intent).
|
||||
consumeUpgradeExtra(intent)
|
||||
// Warm-start intents delivered via onNewIntent. Consuming them here (instead of
|
||||
// directly from onNewIntent) guarantees navCtrl.setup() has run.
|
||||
warmIntents.collect { newIntent -> consumeUpgradeExtra(newIntent) }
|
||||
}
|
||||
|
||||
CapodTheme(state = themeState) {
|
||||
@@ -132,7 +142,9 @@ class MainActivity : Activity2() {
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
consumeUpgradeExtra(intent)
|
||||
setIntent(intent)
|
||||
// Defer to the Compose-side collector — navCtrl may not yet be set up here.
|
||||
warmIntents.tryEmit(intent)
|
||||
}
|
||||
|
||||
private fun consumeUpgradeExtra(intent: Intent?) {
|
||||
|
||||
@@ -64,6 +64,8 @@ import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
|
||||
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 eu.darken.capod.reaction.core.charged.ChargedSlotScope
|
||||
import eu.darken.capod.reaction.core.conversation.ConversationAction
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
@@ -148,6 +150,8 @@ fun DeviceSettingsScreenHost(
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onAncModeChange = { vm.setAncMode(it) },
|
||||
onConversationalAwarenessChange = { vm.setConversationalAwareness(it) },
|
||||
onConversationActionChange = { vm.setConversationAction(it) },
|
||||
onConversationVolumeReductionChange = { vm.setConversationVolumeReduction(it) },
|
||||
onNcWithOneAirPodChange = { vm.setNcWithOneAirPod(it) },
|
||||
onPersonalizedVolumeChange = { vm.setPersonalizedVolume(it) },
|
||||
onToneVolumeChange = { vm.setToneVolume(it) },
|
||||
@@ -167,11 +171,14 @@ fun DeviceSettingsScreenHost(
|
||||
onOnePodModeChange = { vm.setOnePodMode(it) },
|
||||
onAutoPlayChange = { vm.setAutoPlay(it) },
|
||||
onAutoPauseChange = { vm.setAutoPause(it) },
|
||||
onStartMusicOnWearChange = { vm.setStartMusicOnWear(it) },
|
||||
onAutoConnectChange = { vm.setAutoConnect(it) },
|
||||
onAutoConnectConditionChange = { vm.setAutoConnectCondition(it) },
|
||||
onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) },
|
||||
onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) },
|
||||
onFixMonitorMode = { vm.setMonitorModeAutomatic() },
|
||||
onNotifyWhenChargedChange = { vm.setNotifyWhenCharged(it) },
|
||||
onChargedThresholdChange = { vm.setChargedThreshold(it) },
|
||||
onChargedSlotScopeChange = { vm.setChargedSlotScope(it) },
|
||||
onOpenIssueTracker = { vm.openIssueTracker() },
|
||||
onOpenAapTracker = { vm.openAapCompatibilityTracker() },
|
||||
)
|
||||
@@ -186,6 +193,8 @@ fun DeviceSettingsScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {},
|
||||
onConversationalAwarenessChange: (Boolean) -> Unit = {},
|
||||
onConversationActionChange: (ConversationAction) -> Unit = {},
|
||||
onConversationVolumeReductionChange: (Int) -> Unit = {},
|
||||
onNcWithOneAirPodChange: (Boolean) -> Unit = {},
|
||||
onPersonalizedVolumeChange: (Boolean) -> Unit = {},
|
||||
onToneVolumeChange: (Int) -> Unit = {},
|
||||
@@ -205,11 +214,14 @@ fun DeviceSettingsScreen(
|
||||
onOnePodModeChange: (Boolean) -> Unit = {},
|
||||
onAutoPlayChange: (Boolean) -> Unit = {},
|
||||
onAutoPauseChange: (Boolean) -> Unit = {},
|
||||
onStartMusicOnWearChange: (Boolean) -> Unit = {},
|
||||
onAutoConnectChange: (Boolean) -> Unit = {},
|
||||
onAutoConnectConditionChange: (AutoConnectCondition) -> Unit = {},
|
||||
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
||||
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
||||
onFixMonitorMode: () -> Unit = {},
|
||||
onNotifyWhenChargedChange: (Boolean) -> Unit = {},
|
||||
onChargedThresholdChange: (Int) -> Unit = {},
|
||||
onChargedSlotScopeChange: (ChargedSlotScope) -> Unit = {},
|
||||
onOpenIssueTracker: () -> Unit = {},
|
||||
onOpenAapTracker: () -> Unit = {},
|
||||
) {
|
||||
@@ -339,17 +351,21 @@ fun DeviceSettingsScreen(
|
||||
device = device,
|
||||
features = features,
|
||||
isPro = isPro,
|
||||
monitorMode = state.monitorMode,
|
||||
onAutoPlayChange = onAutoPlayChange,
|
||||
onAutoPauseChange = onAutoPauseChange,
|
||||
onStartMusicOnWearChange = onStartMusicOnWearChange,
|
||||
onOnePodModeChange = onOnePodModeChange,
|
||||
onConversationalAwarenessChange = onConversationalAwarenessChange,
|
||||
onConversationActionChange = onConversationActionChange,
|
||||
onConversationVolumeReductionChange = onConversationVolumeReductionChange,
|
||||
onSleepDetectionChange = onSleepDetectionChange,
|
||||
onAutoConnectChange = onAutoConnectChange,
|
||||
onAutoConnectConditionChange = onAutoConnectConditionChange,
|
||||
onShowPopUpOnCaseOpenChange = onShowPopUpOnCaseOpenChange,
|
||||
onShowPopUpOnConnectionChange = onShowPopUpOnConnectionChange,
|
||||
onFixMonitorMode = onFixMonitorMode,
|
||||
onNotifyWhenChargedChange = onNotifyWhenChargedChange,
|
||||
onChargedThresholdChange = onChargedThresholdChange,
|
||||
onChargedSlotScopeChange = onChargedSlotScopeChange,
|
||||
onOpenIssueTracker = onOpenIssueTracker,
|
||||
)
|
||||
}
|
||||
|
||||
+60
-22
@@ -5,8 +5,10 @@ import eu.darken.capod.common.SystemTimeSource
|
||||
import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.bluetooth.NudgeAttemptResult
|
||||
import eu.darken.capod.common.bluetooth.NudgeAvailability
|
||||
import eu.darken.capod.common.bluetooth.NudgeCapabilityStore
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.datastore.value
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -16,9 +18,9 @@ import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.common.upgrade.isPro
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||
import eu.darken.capod.monitor.core.MonitorModeResolver
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.resolvedAncCycleMask
|
||||
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
||||
@@ -29,6 +31,8 @@ import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.ProfileId
|
||||
import eu.darken.capod.profiles.core.ReactionConfig
|
||||
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
|
||||
import eu.darken.capod.reaction.core.charged.ChargedSlotScope
|
||||
import eu.darken.capod.reaction.core.conversation.ConversationAction
|
||||
import eu.darken.capod.reaction.core.stem.StemAction
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -51,7 +55,8 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
private val upgradeRepo: UpgradeRepo,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val monitorModeResolver: MonitorModeResolver,
|
||||
private val nudgeCapabilityStore: NudgeCapabilityStore,
|
||||
private val timeSource: TimeSource,
|
||||
private val webpageTool: WebpageTool,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
@@ -114,8 +119,9 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
// The Bluetooth HEADSET profile lookup can be slow to produce its first value.
|
||||
// Seed this branch so the screen can render immediately after navigation.
|
||||
bluetoothManager.connectedDevices.onStart { emit(emptyList()) },
|
||||
generalSettings.monitorMode.flow,
|
||||
monitorModeResolver.effectiveMode,
|
||||
profilesRepo.profiles,
|
||||
nudgeCapabilityStore.availability,
|
||||
) { args ->
|
||||
val device = args[1] as PodDevice?
|
||||
val upgrade = args[2] as UpgradeRepo.Info
|
||||
@@ -127,6 +133,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val profiles = args[6] as List<eu.darken.capod.profiles.core.DeviceProfile>
|
||||
val nudgeAvailability = args[7] as NudgeAvailability
|
||||
val stemActions = profiles.filterIsInstance<AppleDeviceProfile>()
|
||||
.firstOrNull { it.id == profileId }
|
||||
?.stemActions
|
||||
@@ -142,7 +149,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
device = device,
|
||||
now = timeSource.now(),
|
||||
isPro = upgrade.isPro,
|
||||
isNudgeAvailable = bluetoothManager.isNudgeAvailable,
|
||||
isNudgeAvailable = nudgeAvailability != NudgeAvailability.BROKEN,
|
||||
isForceConnecting = forcing,
|
||||
isClassicallyConnected = device?.address?.let { it in connectedAddresses } == true,
|
||||
monitorMode = monitorMode,
|
||||
@@ -206,18 +213,19 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
events.tryEmit(Event.OpenBluetoothSettings)
|
||||
return@launch
|
||||
}
|
||||
if (!bluetoothManager.isNudgeAvailable) {
|
||||
if (nudgeCapabilityStore.availability.value == NudgeAvailability.BROKEN) {
|
||||
events.tryEmit(Event.OpenBluetoothSettings)
|
||||
return@launch
|
||||
}
|
||||
val accepted = try {
|
||||
val result = try {
|
||||
bluetoothManager.nudgeConnection(bonded)
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "nudgeConnection threw: ${e.message}" }
|
||||
false
|
||||
NudgeAttemptResult.Rejected
|
||||
}
|
||||
log(TAG, INFO) { "nudgeConnection($bonded) accepted=$accepted" }
|
||||
if (!accepted) {
|
||||
log(TAG, INFO) { "nudgeConnection($bonded) result=$result" }
|
||||
nudgeCapabilityStore.record(result)
|
||||
if (result != NudgeAttemptResult.Accepted) {
|
||||
events.tryEmit(Event.OpenBluetoothSettings)
|
||||
}
|
||||
} finally {
|
||||
@@ -382,6 +390,15 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
syncEarDetection(autoPause = enabled)
|
||||
}
|
||||
|
||||
fun setStartMusicOnWear(enabled: Boolean) = launch {
|
||||
log(TAG, INFO) { "setStartMusicOnWear($enabled)" }
|
||||
if (enabled && !upgradeRepo.isPro()) {
|
||||
navTo(Nav.Main.Upgrade)
|
||||
return@launch
|
||||
}
|
||||
updateProfileNow { it.copy(startMusicOnWear = enabled) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the device-side Automatic Ear Detection setting in sync with the
|
||||
* auto-play / auto-pause reaction toggles. When either reaction is active
|
||||
@@ -405,15 +422,6 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
fun setAutoConnect(enabled: Boolean) = launch {
|
||||
log(TAG, INFO) { "setAutoConnect($enabled)" }
|
||||
updateProfileNow { it.copy(autoConnect = enabled) }
|
||||
if (enabled) {
|
||||
// Auto-connect requires ALWAYS mode to react to BLE/case/ear events when
|
||||
// nothing is connected. We intentionally do NOT revert on disable — the user
|
||||
// may have set ALWAYS manually, or another profile may still need it.
|
||||
if (generalSettings.monitorMode.value() != MonitorMode.ALWAYS) {
|
||||
log(TAG, INFO) { "Forcing monitorMode to ALWAYS because autoConnect was enabled" }
|
||||
generalSettings.monitorMode.value(MonitorMode.ALWAYS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoConnectCondition(condition: AutoConnectCondition) = launch {
|
||||
@@ -431,11 +439,41 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
proGatedReaction(enabled) { it.copy(showPopUpOnConnection = enabled) }
|
||||
}
|
||||
|
||||
fun setMonitorModeAutomatic() = launch {
|
||||
log(TAG, INFO) { "setMonitorModeAutomatic()" }
|
||||
generalSettings.monitorMode.value(MonitorMode.AUTOMATIC)
|
||||
fun setNotifyWhenCharged(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setNotifyWhenCharged($enabled)" }
|
||||
proGatedReaction(enabled) { it.copy(notifyWhenCharged = enabled) }
|
||||
}
|
||||
|
||||
fun setChargedSlotScope(scope: ChargedSlotScope) = launch {
|
||||
log(TAG, INFO) { "setChargedSlotScope($scope)" }
|
||||
updateProfileNow { it.copy(chargedSlotScope = scope) }
|
||||
}
|
||||
|
||||
fun setChargedThreshold(percent: Int) = launch {
|
||||
log(TAG, INFO) { "setChargedThreshold($percent)" }
|
||||
val snapped = (percent.toFloat() / ReactionConfig.CHARGED_THRESHOLD_STEP)
|
||||
.let { Math.round(it) * ReactionConfig.CHARGED_THRESHOLD_STEP }
|
||||
.coerceIn(ReactionConfig.MIN_CHARGED_THRESHOLD, ReactionConfig.MAX_CHARGED_THRESHOLD)
|
||||
updateProfileNow { it.copy(chargedThreshold = snapped) }
|
||||
}
|
||||
|
||||
fun setConversationAction(action: ConversationAction) = launch {
|
||||
log(TAG, INFO) { "setConversationAction($action)" }
|
||||
// The action picker is only shown while Conversation Awareness is already enabled (the pod
|
||||
// emits no speaking frames otherwise), so no need to auto-enable it here.
|
||||
if (action != ConversationAction.NOTHING && !upgradeRepo.isPro()) {
|
||||
navTo(Nav.Main.Upgrade)
|
||||
return@launch
|
||||
}
|
||||
updateProfileNow { it.copy(conversationAction = action) }
|
||||
}
|
||||
|
||||
fun setConversationVolumeReduction(percent: Int) = launch {
|
||||
log(TAG, INFO) { "setConversationVolumeReduction($percent)" }
|
||||
updateProfileNow { it.copy(conversationVolumeReduction = percent) }
|
||||
}
|
||||
|
||||
|
||||
fun navToPressControls() = launch {
|
||||
log(TAG, INFO) { "navToPressControls()" }
|
||||
val profileId = targetProfileId.value ?: return@launch
|
||||
|
||||
@@ -4,12 +4,15 @@ import android.os.Build
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.automirrored.twotone.VolumeDown
|
||||
import androidx.compose.material.icons.twotone.BatteryChargingFull
|
||||
import androidx.compose.material.icons.twotone.BluetoothConnected
|
||||
import androidx.compose.material.icons.twotone.Hearing
|
||||
import androidx.compose.material.icons.twotone.LooksOne
|
||||
import androidx.compose.material.icons.twotone.Nightlight
|
||||
import androidx.compose.material.icons.twotone.PauseCircle
|
||||
import androidx.compose.material.icons.twotone.PlayCircle
|
||||
import androidx.compose.material.icons.twotone.RecordVoiceOver
|
||||
import androidx.compose.material.icons.twotone.Workspaces
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -18,6 +21,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -29,39 +33,51 @@ import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import eu.darken.capod.common.settings.InfoBoxType
|
||||
import eu.darken.capod.common.settings.SettingsBaseItem
|
||||
import eu.darken.capod.common.settings.SettingsCategoryHeader
|
||||
import eu.darken.capod.common.settings.SettingsInfoBox
|
||||
import eu.darken.capod.common.settings.SettingsSection
|
||||
import eu.darken.capod.common.settings.SettingsSliderItem
|
||||
import eu.darken.capod.common.settings.SettingsSwitchItem
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.ui.devicesettings.dialogs.AutoConnectConditionDialog
|
||||
import eu.darken.capod.main.ui.devicesettings.dialogs.ChargedSlotScopeDialog
|
||||
import eu.darken.capod.main.ui.devicesettings.dialogs.ConversationActionDialog
|
||||
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
|
||||
import eu.darken.capod.profiles.core.ReactionConfig
|
||||
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
|
||||
import eu.darken.capod.reaction.core.charged.ChargedSlotScope
|
||||
import eu.darken.capod.reaction.core.conversation.ConversationAction
|
||||
|
||||
@Composable
|
||||
internal fun ReactionsCard(
|
||||
device: PodDevice,
|
||||
features: PodModel.Features,
|
||||
isPro: Boolean,
|
||||
monitorMode: MonitorMode,
|
||||
onAutoPlayChange: (Boolean) -> Unit = {},
|
||||
onAutoPauseChange: (Boolean) -> Unit = {},
|
||||
onStartMusicOnWearChange: (Boolean) -> Unit = {},
|
||||
onOnePodModeChange: (Boolean) -> Unit = {},
|
||||
onConversationalAwarenessChange: (Boolean) -> Unit = {},
|
||||
onConversationActionChange: (ConversationAction) -> Unit = {},
|
||||
onConversationVolumeReductionChange: (Int) -> Unit = {},
|
||||
onSleepDetectionChange: (Boolean) -> Unit = {},
|
||||
onAutoConnectChange: (Boolean) -> Unit = {},
|
||||
onAutoConnectConditionChange: (AutoConnectCondition) -> Unit = {},
|
||||
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
||||
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
||||
onFixMonitorMode: () -> Unit = {},
|
||||
onNotifyWhenChargedChange: (Boolean) -> Unit = {},
|
||||
onChargedThresholdChange: (Int) -> Unit = {},
|
||||
onChargedSlotScopeChange: (ChargedSlotScope) -> Unit = {},
|
||||
onOpenIssueTracker: () -> Unit = {},
|
||||
) {
|
||||
val reactions = device.reactions
|
||||
val enabled = device.isAapReady
|
||||
|
||||
var showAutoConnectConditionDialog by remember { mutableStateOf(false) }
|
||||
var showConversationActionDialog by remember { mutableStateOf(false) }
|
||||
var showChargedScopeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
SettingsSection(title = stringResource(R.string.settings_reaction_label)) {
|
||||
if (features.hasEarDetection) {
|
||||
@@ -73,6 +89,16 @@ internal fun ReactionsCard(
|
||||
onCheckedChange = onAutoPlayChange,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
if (reactions.autoPlay) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.PlayCircle,
|
||||
title = stringResource(R.string.settings_start_music_on_wear_label),
|
||||
subtitle = stringResource(R.string.settings_start_music_on_wear_description),
|
||||
checked = reactions.startMusicOnWear,
|
||||
onCheckedChange = onStartMusicOnWearChange,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
}
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.PauseCircle,
|
||||
title = stringResource(R.string.settings_autopause_label),
|
||||
@@ -117,20 +143,49 @@ internal fun ReactionsCard(
|
||||
}
|
||||
if (device.isAapConnected) {
|
||||
val convAwareness = device.conversationalAwareness
|
||||
val hasAnyAapReaction =
|
||||
(features.hasConversationAwareness && convAwareness != null) ||
|
||||
features.hasSleepDetection
|
||||
if (features.hasConversationAwareness && convAwareness != null) {
|
||||
val showConversationAwareness = features.hasConversationAwareness && convAwareness != null
|
||||
val hasAnyAapReaction = showConversationAwareness || features.hasSleepDetection
|
||||
if (showConversationAwareness) {
|
||||
SettingsCategoryHeader(text = stringResource(R.string.conversation_awareness_label))
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.Hearing,
|
||||
title = stringResource(R.string.conversation_awareness_label),
|
||||
subtitle = stringResource(R.string.device_settings_conversation_awareness_description),
|
||||
checked = convAwareness.enabled,
|
||||
checked = convAwareness?.enabled == true,
|
||||
onCheckedChange = onConversationalAwarenessChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
// The "when you start speaking" reaction only fires while CA is on (the pod emits no
|
||||
// speaking frames otherwise), so only surface it once CA is actually enabled.
|
||||
if (convAwareness?.enabled == true) {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.TwoTone.RecordVoiceOver,
|
||||
title = stringResource(R.string.settings_conversation_action_label),
|
||||
subtitle = stringResource(reactions.conversationAction.labelRes),
|
||||
onClick = { showConversationActionDialog = true },
|
||||
enabled = enabled,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
if (reactions.conversationAction == ConversationAction.LOWER_VOLUME) {
|
||||
var sliderValue by remember(reactions.conversationVolumeReduction) {
|
||||
mutableIntStateOf(reactions.conversationVolumeReduction)
|
||||
}
|
||||
SettingsSliderItem(
|
||||
icon = Icons.AutoMirrored.TwoTone.VolumeDown,
|
||||
title = stringResource(R.string.settings_conversation_volume_reduction_label),
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { sliderValue = it.toInt() },
|
||||
onValueChangeFinished = { onConversationVolumeReductionChange(sliderValue) },
|
||||
valueRange = ReactionConfig.MIN_CONVERSATION_VOLUME_REDUCTION.toFloat()..
|
||||
ReactionConfig.MAX_CONVERSATION_VOLUME_REDUCTION.toFloat(),
|
||||
enabled = enabled,
|
||||
valueLabel = { "${it.toInt()}%" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (features.hasSleepDetection) {
|
||||
if (showConversationAwareness) ReactionsDivider()
|
||||
val sleepDet = device.sleepDetection
|
||||
?: AapSetting.SleepDetection(enabled = true)
|
||||
SettingsSwitchItem(
|
||||
@@ -198,21 +253,67 @@ internal fun ReactionsCard(
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
val anyPopupEnabled = reactions.showPopUpOnCaseOpen || reactions.showPopUpOnConnection
|
||||
if (anyPopupEnabled && monitorMode == MonitorMode.MANUAL) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_popup_warning_manual_mode),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onFixMonitorMode) {
|
||||
Text(stringResource(R.string.general_fix_it_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (anyPopupEnabled) {
|
||||
if (anyPopupEnabled) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_popup_info_not_in_app),
|
||||
)
|
||||
}
|
||||
ReactionsDivider()
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.BatteryChargingFull,
|
||||
title = stringResource(R.string.settings_charged_notification_label),
|
||||
subtitle = stringResource(R.string.settings_charged_notification_description),
|
||||
checked = reactions.notifyWhenCharged,
|
||||
onCheckedChange = onNotifyWhenChargedChange,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
if (reactions.notifyWhenCharged) {
|
||||
var thresholdValue by remember(reactions.chargedThreshold) {
|
||||
mutableIntStateOf(reactions.chargedThreshold)
|
||||
}
|
||||
SettingsSliderItem(
|
||||
icon = Icons.TwoTone.BatteryChargingFull,
|
||||
title = stringResource(R.string.settings_charged_threshold_label),
|
||||
value = thresholdValue.toFloat(),
|
||||
onValueChange = { thresholdValue = it.toInt() },
|
||||
onValueChangeFinished = { onChargedThresholdChange(thresholdValue) },
|
||||
valueRange = ReactionConfig.MIN_CHARGED_THRESHOLD.toFloat()..
|
||||
ReactionConfig.MAX_CHARGED_THRESHOLD.toFloat(),
|
||||
steps = (ReactionConfig.MAX_CHARGED_THRESHOLD - ReactionConfig.MIN_CHARGED_THRESHOLD) /
|
||||
ReactionConfig.CHARGED_THRESHOLD_STEP - 1,
|
||||
valueLabel = { "${it.toInt()}%" },
|
||||
)
|
||||
if (features.hasCase) {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.TwoTone.Workspaces,
|
||||
title = stringResource(R.string.settings_charged_scope_label),
|
||||
subtitle = stringResource(reactions.chargedSlotScope.labelRes),
|
||||
onClick = { showChargedScopeDialog = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showConversationActionDialog) {
|
||||
ConversationActionDialog(
|
||||
current = reactions.conversationAction,
|
||||
onSelect = {
|
||||
onConversationActionChange(it)
|
||||
showConversationActionDialog = false
|
||||
},
|
||||
onDismiss = { showConversationActionDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showChargedScopeDialog) {
|
||||
ChargedSlotScopeDialog(
|
||||
current = reactions.chargedSlotScope,
|
||||
onSelect = {
|
||||
onChargedSlotScopeChange(it)
|
||||
showChargedScopeDialog = false
|
||||
},
|
||||
onDismiss = { showChargedScopeDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showAutoConnectConditionDialog) {
|
||||
@@ -246,7 +347,6 @@ private fun ReactionsCardPreview() = PreviewWrapper {
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
monitorMode = state.monitorMode,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -259,6 +359,5 @@ private fun ReactionsCardNonProPreview() = PreviewWrapper {
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
monitorMode = state.monitorMode,
|
||||
)
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
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.semantics.Role
|
||||
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.reaction.core.charged.ChargedSlotScope
|
||||
|
||||
@Composable
|
||||
internal fun ChargedSlotScopeDialog(
|
||||
current: ChargedSlotScope,
|
||||
onSelect: (ChargedSlotScope) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.settings_charged_scope_label)) },
|
||||
text = {
|
||||
Column(Modifier.selectableGroup()) {
|
||||
ChargedSlotScope.entries.forEach { scope ->
|
||||
val isSelected = scope == current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { onSelect(scope) },
|
||||
role = Role.RadioButton,
|
||||
)
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(selected = isSelected, onClick = null)
|
||||
Text(
|
||||
text = stringResource(scope.labelRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ChargedSlotScopeDialogPreview() = PreviewWrapper {
|
||||
ChargedSlotScopeDialog(
|
||||
current = ChargedSlotScope.PODS_AND_CASE,
|
||||
onSelect = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
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.semantics.Role
|
||||
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.reaction.core.conversation.ConversationAction
|
||||
|
||||
@Composable
|
||||
internal fun ConversationActionDialog(
|
||||
current: ConversationAction,
|
||||
onSelect: (ConversationAction) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.settings_conversation_action_label)) },
|
||||
text = {
|
||||
Column(Modifier.selectableGroup()) {
|
||||
ConversationAction.entries.forEach { action ->
|
||||
val isSelected = action == current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { onSelect(action) },
|
||||
role = Role.RadioButton,
|
||||
)
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(selected = isSelected, onClick = null)
|
||||
Text(
|
||||
text = stringResource(action.labelRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ConversationActionDialogPreview() = PreviewWrapper {
|
||||
ConversationActionDialog(
|
||||
current = ConversationAction.LOWER_VOLUME,
|
||||
onSelect = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
@@ -61,6 +61,7 @@ import eu.darken.capod.main.ui.overview.cards.NoProfilesCard
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCard
|
||||
import eu.darken.capod.main.ui.overview.cards.ReactionsMovedHintCard
|
||||
import eu.darken.capod.main.ui.overview.cards.SinglePodsCard
|
||||
import eu.darken.capod.main.ui.overview.cards.TroubleshootSuggestionCard
|
||||
import eu.darken.capod.main.ui.overview.cards.UnknownPodDeviceCard
|
||||
import eu.darken.capod.main.ui.overview.cards.UnmatchedDevicesCard
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
@@ -172,6 +173,7 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) {
|
||||
},
|
||||
onManageDevices = { vm.goToDeviceManager() },
|
||||
onSettings = { vm.goToSettings() },
|
||||
onTroubleShooter = { vm.goToTroubleShooter() },
|
||||
onUpgrade = { vm.onUpgrade() },
|
||||
onToggleUnmatched = { vm.toggleUnmatchedDevices() },
|
||||
onAncModeChange = { device, mode -> vm.setAncMode(device, mode) },
|
||||
@@ -194,6 +196,7 @@ fun OverviewScreen(
|
||||
onBluetoothSettings: () -> Unit,
|
||||
onManageDevices: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
onTroubleShooter: () -> Unit = {},
|
||||
onUpgrade: () -> Unit,
|
||||
onToggleUnmatched: () -> Unit,
|
||||
onAncModeChange: (PodDevice, AapSetting.AncMode.Value) -> Unit = { _, _ -> },
|
||||
@@ -327,7 +330,7 @@ fun OverviewScreen(
|
||||
PodDeviceCard(
|
||||
device = device,
|
||||
isPro = state.upgradeInfo.isPro,
|
||||
showDebug = state.isDebugMode,
|
||||
showDebug = state.isDebug,
|
||||
now = state.now,
|
||||
isCollapsed = isCollapsed,
|
||||
onToggleCollapse = if (isToggleable) {
|
||||
@@ -351,18 +354,25 @@ fun OverviewScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 4c. Troubleshooter suggestion — connected via audio but no live data is reaching us
|
||||
if (state.showTroubleshootSuggestion) {
|
||||
item(key = "troubleshoot_suggestion") {
|
||||
TroubleshootSuggestionCard(onTroubleShooter = onTroubleShooter)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Monitoring active card
|
||||
if (state.profiles.isNotEmpty() && state.devices.isEmpty()) {
|
||||
if (state.profiles.isNotEmpty() && state.profiledDevices.isEmpty() && !state.shouldShowUnmatchedSection) {
|
||||
item(key = "monitoring_active") {
|
||||
MonitoringActiveCard()
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Unmatched devices section
|
||||
if (state.unmatchedDevices.isNotEmpty()) {
|
||||
if (state.shouldShowUnmatchedSection) {
|
||||
item(key = "unmatched_header") {
|
||||
UnmatchedDevicesCard(
|
||||
count = state.unmatchedDevices.size,
|
||||
count = state.visibleUnmatchedDevices.size,
|
||||
isExpanded = state.showUnmatchedDevices,
|
||||
onToggle = onToggleUnmatched,
|
||||
)
|
||||
@@ -370,13 +380,13 @@ fun OverviewScreen(
|
||||
|
||||
if (state.showUnmatchedDevices) {
|
||||
itemsIndexed(
|
||||
items = state.unmatchedDevices,
|
||||
items = state.visibleUnmatchedDevices,
|
||||
key = { index, device -> unmatchedDeviceKey(device, index) },
|
||||
) { _, device ->
|
||||
PodDeviceCard(
|
||||
device = device,
|
||||
isPro = state.upgradeInfo.isPro,
|
||||
showDebug = state.isDebugMode,
|
||||
showDebug = state.isDebug,
|
||||
now = state.now,
|
||||
onAncModeChange = { mode -> onAncModeChange(device, mode) },
|
||||
onUpgrade = onUpgrade,
|
||||
@@ -438,7 +448,7 @@ private fun OverviewScreenWithDevicesPreview() = PreviewWrapper {
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
),
|
||||
isDebugMode = false,
|
||||
isDebug = false,
|
||||
isBluetoothEnabled = true,
|
||||
profiles = listOf(
|
||||
MockPodDataProvider.profile("My AirPods Pro", PodModel.AIRPODS_PRO2),
|
||||
@@ -465,7 +475,7 @@ private fun OverviewScreenEmptyPreview() = PreviewWrapper {
|
||||
now = SystemTimeSource.now(),
|
||||
permissions = emptySet(),
|
||||
devices = emptyList(),
|
||||
isDebugMode = false,
|
||||
isDebug = false,
|
||||
isBluetoothEnabled = true,
|
||||
profiles = listOf(MockPodDataProvider.profile("My AirPods", PodModel.AIRPODS_PRO2)),
|
||||
upgradeInfo = MockPodDataProvider.fossInfo(),
|
||||
@@ -488,7 +498,7 @@ private fun OverviewScreenNoProfilesPreview() = PreviewWrapper {
|
||||
now = SystemTimeSource.now(),
|
||||
permissions = emptySet(),
|
||||
devices = emptyList(),
|
||||
isDebugMode = false,
|
||||
isDebug = false,
|
||||
isBluetoothEnabled = true,
|
||||
profiles = emptyList(),
|
||||
upgradeInfo = MockPodDataProvider.gplayInfo(),
|
||||
@@ -511,7 +521,7 @@ private fun OverviewScreenBluetoothOffPreview() = PreviewWrapper {
|
||||
now = SystemTimeSource.now(),
|
||||
permissions = emptySet(),
|
||||
devices = emptyList(),
|
||||
isDebugMode = false,
|
||||
isDebug = false,
|
||||
isBluetoothEnabled = false,
|
||||
profiles = listOf(MockPodDataProvider.profile("My AirPods", PodModel.AIRPODS_PRO2)),
|
||||
upgradeInfo = MockPodDataProvider.fossInfo(isPro = true),
|
||||
|
||||
@@ -5,8 +5,7 @@ import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.datastore.value
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -22,6 +21,7 @@ import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||
import eu.darken.capod.monitor.core.MonitorModeResolver
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.tierRank
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
@@ -34,12 +34,16 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.combine as combineFlows
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -50,11 +54,11 @@ class OverviewViewModel @Inject constructor(
|
||||
private val deviceMonitor: DeviceMonitor,
|
||||
private val permissionTool: PermissionTool,
|
||||
private val generalSettings: GeneralSettings,
|
||||
debugSettings: DebugSettings,
|
||||
private val upgradeRepo: UpgradeRepo,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val aapManager: AapConnectionManager,
|
||||
private val monitorModeResolver: MonitorModeResolver,
|
||||
private val timeSource: TimeSource,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
@@ -77,29 +81,83 @@ class OverviewViewModel @Inject constructor(
|
||||
private val showUnmatchedDevices = MutableStateFlow(false)
|
||||
private val userExpansionOverrides = MutableStateFlow<Set<String>>(emptySet())
|
||||
|
||||
val workerAutolaunch = permissionTool.missingScanPermissions
|
||||
.onEach { permissions ->
|
||||
if (permissions.isNotEmpty()) {
|
||||
log(TAG) { "Missing scan permissions: $permissions" }
|
||||
private data class OverviewUiSettings(
|
||||
val reactionsHintDismissed: Boolean,
|
||||
val hideUnmatchedDevices: Boolean,
|
||||
val showTroubleshootSuggestion: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* True when a profile is connected to the system (audio) but CAPod has *no* live data for any
|
||||
* device — the classic "broadcasts are being dropped by the ROM" symptom (e.g. some HyperOS
|
||||
* phones, see #603). Surfaced as a hint pointing at the Troubleshooter, which can probe and
|
||||
* persist a working compatibility combo. Debounced so it doesn't flash during the brief window
|
||||
* between an audio connection and the first BLE broadcast. Suppressed whenever any pod is live,
|
||||
* so it never claims "no data" while data is visibly arriving (incl. the #603 duplicate state).
|
||||
*/
|
||||
private val troubleshootSuggestion = combineFlows(
|
||||
profilesRepo.profiles,
|
||||
bluetoothManager.connectedDevices.onStart { emit(emptyList()) },
|
||||
deviceMonitor.devices,
|
||||
permissionTool.missingScanPermissions,
|
||||
) { profiles, connectedDevices, devices, missingScanPermissions ->
|
||||
val connectedAddresses = connectedDevices.mapTo(mutableSetOf()) { it.address }
|
||||
val anyProfileConnected = profiles.any { it.address != null && it.address in connectedAddresses }
|
||||
val anyLivePod = devices.any { it.isLive }
|
||||
missingScanPermissions.isEmpty() && anyProfileConnected && !anyLivePod
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { conditionMet ->
|
||||
if (conditionMet) flow {
|
||||
delay(TROUBLESHOOT_SUGGESTION_DELAY_MS)
|
||||
emit(true)
|
||||
} else flowOf(false)
|
||||
}
|
||||
.onStart { emit(false) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val overviewUiSettings = combineFlows(
|
||||
generalSettings.reactionsHintDismissed.flow,
|
||||
generalSettings.hideUnmatchedDevices.flow,
|
||||
troubleshootSuggestion,
|
||||
) { reactionsHintDismissed, hideUnmatched, showTroubleshootSuggestion ->
|
||||
OverviewUiSettings(reactionsHintDismissed, hideUnmatched, showTroubleshootSuggestion)
|
||||
}
|
||||
|
||||
init {
|
||||
// When the persistent "hide unmatched" setting is enabled, reset the in-session expand
|
||||
// toggle so the section reappears collapsed if the user later disables the setting.
|
||||
launch {
|
||||
generalSettings.hideUnmatchedDevices.flow
|
||||
.filter { it }
|
||||
.collect { showUnmatchedDevices.value = false }
|
||||
}
|
||||
}
|
||||
|
||||
val workerAutolaunch = combine(
|
||||
permissionTool.missingScanPermissions,
|
||||
monitorModeResolver.effectiveMode,
|
||||
// BluetoothManager2.connectedDevices is gated by a slow HEADSET-profile lookup. Without
|
||||
// an onStart seed the combine wouldn't fire until that profile is bound, so ALWAYS mode
|
||||
// would silently delay autolaunch until the first connected-device emission.
|
||||
bluetoothManager.connectedDevices
|
||||
.onStart { emit(emptyList()) }
|
||||
.map { it.isNotEmpty() }
|
||||
.distinctUntilChanged(),
|
||||
) { missing, mode, anyConnected -> Triple(missing, mode, anyConnected) }
|
||||
.distinctUntilChanged()
|
||||
.onEach { (missing, mode, anyConnected) ->
|
||||
if (missing.isNotEmpty()) {
|
||||
log(TAG) { "Missing scan permissions: $missing" }
|
||||
return@onEach
|
||||
}
|
||||
|
||||
val shouldStart = when (generalSettings.monitorMode.valueBlocking) {
|
||||
val shouldStart = when (mode) {
|
||||
MonitorMode.MANUAL -> false
|
||||
MonitorMode.AUTOMATIC -> {
|
||||
try {
|
||||
val devices = withTimeoutOrNull(5_000) { bluetoothManager.connectedDevices.first() }
|
||||
devices?.isNotEmpty() == true
|
||||
} catch (e: SecurityException) {
|
||||
log(TAG) { "Can't check connected devices without BLUETOOTH_CONNECT: ${e.message}" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
MonitorMode.AUTOMATIC -> anyConnected
|
||||
MonitorMode.ALWAYS -> true
|
||||
}
|
||||
if (shouldStart) {
|
||||
log(TAG) { "Starting monitor" }
|
||||
log(TAG) { "Starting monitor (mode=$mode)" }
|
||||
monitorControl.startMonitor()
|
||||
}
|
||||
}
|
||||
@@ -126,15 +184,15 @@ class OverviewViewModel @Inject constructor(
|
||||
updateTicker,
|
||||
permissionTool.missingPermissions,
|
||||
pods,
|
||||
debugSettings.isDebugModeEnabled.flow,
|
||||
Bugs.isDebug,
|
||||
bluetoothManager.isBluetoothEnabled,
|
||||
profilesRepo.profiles,
|
||||
upgradeRepo.upgradeInfo,
|
||||
showUnmatchedDevices,
|
||||
userExpansionOverrides,
|
||||
generalSettings.reactionsHintDismissed.flow,
|
||||
overviewUiSettings,
|
||||
profilesRepo.hadLegacyReactionData,
|
||||
) { _, permissions, devices, isDebugMode, isBluetoothEnabled, profiles, upgradeInfo, showUnmatched, expandedIds, reactionsHintDismissed, hadLegacyReactionData ->
|
||||
) { _, permissions, devices, isDebug, isBluetoothEnabled, profiles, upgradeInfo, showUnmatched, expandedIds, uiSettings, hadLegacyReactionData ->
|
||||
// Prune stale overrides (profiles that no longer exist)
|
||||
val currentProfileIds = profiles.map { it.id }.toSet()
|
||||
val prunedExpandedIds = expandedIds.filter { it in currentProfileIds }.toSet()
|
||||
@@ -143,13 +201,15 @@ class OverviewViewModel @Inject constructor(
|
||||
now = timeSource.now(),
|
||||
permissions = permissions,
|
||||
devices = devices,
|
||||
isDebugMode = isDebugMode,
|
||||
isDebug = isDebug,
|
||||
isBluetoothEnabled = isBluetoothEnabled,
|
||||
profiles = profiles,
|
||||
upgradeInfo = upgradeInfo,
|
||||
showUnmatchedDevices = showUnmatched,
|
||||
userExpandedIds = prunedExpandedIds,
|
||||
showReactionsHint = hadLegacyReactionData && !reactionsHintDismissed,
|
||||
showReactionsHint = hadLegacyReactionData && !uiSettings.reactionsHintDismissed,
|
||||
hideUnmatchedDevices = uiSettings.hideUnmatchedDevices,
|
||||
showTroubleshootSuggestion = uiSettings.showTroubleshootSuggestion,
|
||||
)
|
||||
}.asLiveState()
|
||||
|
||||
@@ -159,13 +219,15 @@ class OverviewViewModel @Inject constructor(
|
||||
val now: Instant,
|
||||
val permissions: Set<Permission>,
|
||||
val devices: List<PodDevice>,
|
||||
val isDebugMode: Boolean,
|
||||
val isDebug: Boolean,
|
||||
val isBluetoothEnabled: Boolean,
|
||||
val profiles: List<DeviceProfile>,
|
||||
val upgradeInfo: UpgradeRepo.Info,
|
||||
val showUnmatchedDevices: Boolean,
|
||||
val userExpandedIds: Set<String> = emptySet(),
|
||||
val showReactionsHint: Boolean = false,
|
||||
val hideUnmatchedDevices: Boolean = false,
|
||||
val showTroubleshootSuggestion: Boolean = false,
|
||||
) {
|
||||
val isScanBlocked: Boolean get() = permissions.any { it.isScanBlocking }
|
||||
|
||||
@@ -186,6 +248,11 @@ class OverviewViewModel @Inject constructor(
|
||||
val hiddenProfiledDeviceCount: Int get() = profiledDevices.size - visibleProfiledDevices.size
|
||||
val unmatchedDevices: List<PodDevice> get() = devices.filter { it.profileId == null }
|
||||
|
||||
/** Unmatched devices actually shown on the dashboard (empty when the hide setting is on). */
|
||||
val visibleUnmatchedDevices: List<PodDevice>
|
||||
get() = if (hideUnmatchedDevices) emptyList() else unmatchedDevices
|
||||
val shouldShowUnmatchedSection: Boolean get() = visibleUnmatchedDevices.isNotEmpty()
|
||||
|
||||
val bluetoothIconState: BluetoothIconState
|
||||
get() = when {
|
||||
isScanBlocked -> BluetoothIconState.HIDDEN
|
||||
@@ -224,6 +291,11 @@ class OverviewViewModel @Inject constructor(
|
||||
navTo(Nav.Settings.Index)
|
||||
}
|
||||
|
||||
fun goToTroubleShooter() {
|
||||
log(TAG, INFO) { "goToTroubleShooter()" }
|
||||
navTo(Nav.Main.TroubleShooter)
|
||||
}
|
||||
|
||||
fun goToDeviceManager() {
|
||||
log(TAG, INFO) { "goToDeviceManager()" }
|
||||
navTo(Nav.Main.DeviceManager)
|
||||
@@ -284,6 +356,14 @@ class OverviewViewModel @Inject constructor(
|
||||
|
||||
companion object {
|
||||
private const val FREE_DEVICE_LIMIT = 1
|
||||
|
||||
/**
|
||||
* How long the "connected but no live data" condition must hold before the troubleshooter
|
||||
* hint appears, so it doesn't flash during the gap between an audio connection and the first
|
||||
* BLE broadcast.
|
||||
*/
|
||||
private const val TROUBLESHOOT_SUGGESTION_DELAY_MS = 15_000L
|
||||
|
||||
private val TAG = logTag("Overview", "VM")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@ import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.HasPodStyle
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.apple.ble.toBatteryFloat
|
||||
import eu.darken.capod.pods.core.apple.ble.toBatteryOrNull
|
||||
import java.time.Instant
|
||||
|
||||
@Composable
|
||||
@@ -227,7 +225,7 @@ private fun ColumnScope.DualPodsCardExpanded(
|
||||
) {
|
||||
PodGauge(
|
||||
iconRes = device.leftPodIcon,
|
||||
batteryPercent = device.batteryLeft.toBatteryFloat(),
|
||||
batteryPercent = device.batteryLeft,
|
||||
chargingState = device.leftPodChargingState
|
||||
?: device.isLeftPodCharging?.let { if (it) AapPodState.ChargingState.CHARGING else null },
|
||||
isInEar = device.isLeftInEar ?: false,
|
||||
@@ -239,7 +237,7 @@ private fun ColumnScope.DualPodsCardExpanded(
|
||||
|
||||
PodGauge(
|
||||
iconRes = device.rightPodIcon,
|
||||
batteryPercent = device.batteryRight.toBatteryFloat(),
|
||||
batteryPercent = device.batteryRight,
|
||||
chargingState = device.rightPodChargingState
|
||||
?: device.isRightPodCharging?.let { if (it) AapPodState.ChargingState.CHARGING else null },
|
||||
isInEar = device.isRightInEar ?: false,
|
||||
@@ -360,7 +358,7 @@ private fun PodGauge(
|
||||
|
||||
// Battery percentage
|
||||
Text(
|
||||
text = formatBatteryPercent(context, batteryPercent.toBatteryOrNull()),
|
||||
text = formatBatteryPercent(context, batteryPercent),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (batteryPercent >= 0f) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
@@ -412,7 +410,7 @@ private fun CaseRow(
|
||||
)
|
||||
|
||||
BatteryCapsule(
|
||||
percent = device.batteryCase.toBatteryFloat(),
|
||||
percent = device.batteryCase,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(8.dp),
|
||||
|
||||
@@ -60,7 +60,9 @@ 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.batteryProgress
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.apple.ble.isKnownBattery
|
||||
import java.time.Instant
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@@ -192,17 +194,18 @@ private fun ColumnScope.SinglePodsCardExpanded(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val clamped = device.batteryHeadset?.coerceIn(0f, 1f)
|
||||
val percent = device.batteryHeadset
|
||||
val isKnown = isKnownBattery(percent)
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = clamped ?: 0f,
|
||||
targetValue = batteryProgress(percent),
|
||||
animationSpec = tween(600, easing = FastOutSlowInEasing),
|
||||
label = "gaugeProgress",
|
||||
)
|
||||
|
||||
val ringColor = when {
|
||||
clamped == null -> MaterialTheme.colorScheme.surfaceVariant
|
||||
clamped > 0.30f -> MaterialTheme.colorScheme.primary
|
||||
clamped >= 0.15f -> MaterialTheme.colorScheme.tertiary
|
||||
!isKnown -> MaterialTheme.colorScheme.surfaceVariant
|
||||
percent > 0.30f -> MaterialTheme.colorScheme.primary
|
||||
percent >= 0.15f -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
|
||||
@@ -237,7 +240,7 @@ private fun ColumnScope.SinglePodsCardExpanded(
|
||||
)
|
||||
|
||||
// Progress ring
|
||||
if (clamped != null) {
|
||||
if (isKnown) {
|
||||
CircularProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = Modifier.size(88.dp),
|
||||
@@ -250,9 +253,9 @@ private fun ColumnScope.SinglePodsCardExpanded(
|
||||
|
||||
// Battery text inside ring
|
||||
Text(
|
||||
text = formatBatteryPercent(context, device.batteryHeadset),
|
||||
text = formatBatteryPercent(context, percent),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = if (device.batteryHeadset != null) {
|
||||
color = if (isKnown) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package eu.darken.capod.main.ui.overview.cards
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.Troubleshoot
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.unit.dp
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
|
||||
@Composable
|
||||
fun TroubleshootSuggestionCard(onTroubleShooter: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Troubleshoot,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.overview_troubleshoot_suggestion_label),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.overview_troubleshoot_suggestion_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = onTroubleShooter,
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
) {
|
||||
Text(text = stringResource(R.string.overview_troubleshoot_suggestion_action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun TroubleshootSuggestionCardPreview() = PreviewWrapper {
|
||||
TroubleshootSuggestionCard(onTroubleShooter = {})
|
||||
}
|
||||
+17
-15
@@ -33,17 +33,19 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ble.batteryProgress
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.apple.ble.isKnownBattery
|
||||
|
||||
@Composable
|
||||
fun CompactBatterySummary(
|
||||
device: PodDevice,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hasAnyBattery = device.batteryLeft != null
|
||||
|| device.batteryRight != null
|
||||
|| device.batteryHeadset != null
|
||||
|| device.batteryCase != null
|
||||
val hasAnyBattery = isKnownBattery(device.batteryLeft)
|
||||
|| isKnownBattery(device.batteryRight)
|
||||
|| isKnownBattery(device.batteryHeadset)
|
||||
|| isKnownBattery(device.batteryCase)
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
@@ -80,7 +82,7 @@ private fun RowScope.DualPodsRow(device: PodDevice) {
|
||||
percent = device.batteryRight,
|
||||
)
|
||||
|
||||
if (device.hasCase && device.batteryCase != null) {
|
||||
if (device.hasCase && isKnownBattery(device.batteryCase)) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
MiniCaseCluster(device = device)
|
||||
}
|
||||
@@ -93,7 +95,7 @@ private fun RowScope.SinglePodRow(device: PodDevice) {
|
||||
iconRes = null,
|
||||
percent = device.batteryHeadset,
|
||||
)
|
||||
if (device.hasCase && device.batteryCase != null) {
|
||||
if (device.hasCase && isKnownBattery(device.batteryCase)) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
MiniCaseCluster(device = device)
|
||||
}
|
||||
@@ -121,21 +123,21 @@ private fun RowScope.EmptyBatteryRow() {
|
||||
@Composable
|
||||
private fun MiniPodRing(
|
||||
iconRes: Int?,
|
||||
percent: Float?,
|
||||
percent: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clamped = percent?.coerceIn(0f, 1f)
|
||||
val isKnown = isKnownBattery(percent)
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = clamped ?: 0f,
|
||||
targetValue = batteryProgress(percent),
|
||||
animationSpec = tween(600, easing = FastOutSlowInEasing),
|
||||
label = "miniGaugeProgress",
|
||||
)
|
||||
|
||||
val ringColor = when {
|
||||
clamped == null -> MaterialTheme.colorScheme.surfaceVariant
|
||||
clamped > 0.30f -> MaterialTheme.colorScheme.primary
|
||||
clamped >= 0.15f -> MaterialTheme.colorScheme.tertiary
|
||||
!isKnown -> MaterialTheme.colorScheme.surfaceVariant
|
||||
percent > 0.30f -> MaterialTheme.colorScheme.primary
|
||||
percent >= 0.15f -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ private fun MiniPodRing(
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
if (clamped != null) {
|
||||
if (isKnown) {
|
||||
CircularProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = Modifier.size(28.dp),
|
||||
@@ -177,7 +179,7 @@ private fun MiniPodRing(
|
||||
Text(
|
||||
text = formatBatteryPercent(context, percent),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = if (percent != null) {
|
||||
color = if (isKnown) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -204,7 +206,7 @@ private fun MiniCaseCluster(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
BatteryCapsule(
|
||||
percent = device.batteryCase ?: -1f,
|
||||
percent = device.batteryCase,
|
||||
modifier = Modifier
|
||||
.width(36.dp)
|
||||
.height(6.dp),
|
||||
|
||||
+17
-101
@@ -1,44 +1,32 @@
|
||||
package eu.darken.capod.main.ui.settings.general
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
||||
import androidx.compose.material.icons.twotone.AccountTree
|
||||
import androidx.compose.material.icons.twotone.BugReport
|
||||
import androidx.compose.material.icons.twotone.Contrast
|
||||
import androidx.compose.material.icons.twotone.DarkMode
|
||||
import androidx.compose.material.icons.twotone.DisabledVisible
|
||||
import androidx.compose.material.icons.twotone.FilterList
|
||||
import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.twotone.Notifications
|
||||
import androidx.compose.material.icons.twotone.Palette
|
||||
import androidx.compose.material.icons.twotone.VisibilityOff
|
||||
import androidx.compose.material.icons.automirrored.twotone.ViewList
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import eu.darken.capod.R
|
||||
@@ -55,7 +43,6 @@ import eu.darken.capod.common.theming.ThemeColor
|
||||
import eu.darken.capod.common.theming.ThemeMode
|
||||
import eu.darken.capod.common.theming.ThemeState
|
||||
import eu.darken.capod.common.theming.ThemeStyle
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
|
||||
@Composable
|
||||
fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) {
|
||||
@@ -67,13 +54,12 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) {
|
||||
GeneralSettingsScreen(
|
||||
state = it,
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onMonitorModeSelected = { mode -> vm.setMonitorMode(mode) },
|
||||
onShowConnectedNotificationChanged = { enabled -> vm.setShowConnectedNotification(enabled) },
|
||||
onKeepNotificationAfterDisconnectChanged = { enabled -> vm.setKeepNotificationAfterDisconnect(enabled) },
|
||||
onDebugSettings = { vm.goToDebugSettings() },
|
||||
onOffloadedFilteringDisabledChanged = { disabled -> vm.setOffloadedFilteringDisabled(disabled) },
|
||||
onOffloadedBatchingDisabledChanged = { disabled -> vm.setOffloadedBatchingDisabled(disabled) },
|
||||
onUseIndirectScanResultCallbackChanged = { enabled -> vm.setUseIndirectScanResultCallback(enabled) },
|
||||
onHideUnmatchedDevicesChanged = { enabled -> vm.setHideUnmatchedDevices(enabled) },
|
||||
onThemeModeSelected = { mode -> vm.setThemeMode(mode) },
|
||||
onThemeStyleSelected = { style -> vm.setThemeStyle(style) },
|
||||
onThemeColorSelected = { color -> vm.setThemeColor(color) },
|
||||
@@ -86,20 +72,18 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) {
|
||||
fun GeneralSettingsScreen(
|
||||
state: GeneralSettingsViewModel.State,
|
||||
onNavigateUp: () -> Unit,
|
||||
onMonitorModeSelected: (MonitorMode) -> Unit,
|
||||
onShowConnectedNotificationChanged: (Boolean) -> Unit,
|
||||
onKeepNotificationAfterDisconnectChanged: (Boolean) -> Unit,
|
||||
onDebugSettings: () -> Unit,
|
||||
onOffloadedFilteringDisabledChanged: (Boolean) -> Unit,
|
||||
onOffloadedBatchingDisabledChanged: (Boolean) -> Unit,
|
||||
onUseIndirectScanResultCallbackChanged: (Boolean) -> Unit,
|
||||
onHideUnmatchedDevicesChanged: (Boolean) -> Unit,
|
||||
onThemeModeSelected: (ThemeMode) -> Unit = {},
|
||||
onThemeStyleSelected: (ThemeStyle) -> Unit = {},
|
||||
onThemeColorSelected: (ThemeColor) -> Unit = {},
|
||||
onUpgrade: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showMonitorModeDialog by remember { mutableStateOf(false) }
|
||||
var showColorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val isMaterialYouActive = state.themeState.style == ThemeStyle.MATERIAL_YOU &&
|
||||
@@ -122,17 +106,6 @@ fun GeneralSettingsScreen(
|
||||
},
|
||||
) { innerPadding ->
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
item {
|
||||
SettingsCategoryHeader(text = stringResource(R.string.settings_category_monitoring_label))
|
||||
}
|
||||
item {
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_monitor_mode_label),
|
||||
subtitle = stringResource(state.monitorMode.labelRes),
|
||||
icon = Icons.TwoTone.DisabledVisible,
|
||||
onClick = { showMonitorModeDialog = true },
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingsCategoryHeader(text = stringResource(R.string.settings_category_appearance_label))
|
||||
}
|
||||
@@ -229,10 +202,17 @@ fun GeneralSettingsScreen(
|
||||
}
|
||||
item {
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_debug_label),
|
||||
subtitle = stringResource(R.string.settings_debug_description),
|
||||
icon = Icons.TwoTone.BugReport,
|
||||
onClick = onDebugSettings,
|
||||
title = stringResource(R.string.settings_overview_hide_unmatched_label),
|
||||
subtitle = stringResource(R.string.settings_overview_hide_unmatched_description),
|
||||
icon = Icons.TwoTone.VisibilityOff,
|
||||
onClick = { onHideUnmatchedDevicesChanged(!state.hideUnmatchedDevices) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.hideUnmatchedDevices,
|
||||
onCheckedChange = onHideUnmatchedDevicesChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
@@ -286,20 +266,6 @@ fun GeneralSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (showMonitorModeDialog) {
|
||||
ListPreferenceDialog(
|
||||
title = stringResource(R.string.settings_monitor_mode_label),
|
||||
entries = MonitorMode.entries,
|
||||
selectedEntry = state.monitorMode,
|
||||
onEntrySelected = {
|
||||
onMonitorModeSelected(it)
|
||||
showMonitorModeDialog = false
|
||||
},
|
||||
entryLabel = { stringResource(it.labelRes) },
|
||||
onDismiss = { showMonitorModeDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showColorDialog) {
|
||||
ThemeColorSelectorDialog(
|
||||
selectedColor = state.themeState.color,
|
||||
@@ -314,12 +280,12 @@ fun GeneralSettingsScreen(
|
||||
|
||||
private fun previewGeneralState(isPro: Boolean) = GeneralSettingsViewModel.State(
|
||||
isPro = isPro,
|
||||
monitorMode = MonitorMode.AUTOMATIC,
|
||||
showConnectedNotification = true,
|
||||
keepNotificationAfterDisconnect = false,
|
||||
isOffloadedFilteringDisabled = false,
|
||||
isOffloadedBatchingDisabled = false,
|
||||
useIndirectScanResultCallback = false,
|
||||
hideUnmatchedDevices = false,
|
||||
themeState = ThemeState(),
|
||||
)
|
||||
|
||||
@@ -329,13 +295,12 @@ private fun GeneralSettingsScreenProPreview() = PreviewWrapper {
|
||||
GeneralSettingsScreen(
|
||||
state = previewGeneralState(isPro = true),
|
||||
onNavigateUp = {},
|
||||
onMonitorModeSelected = {},
|
||||
onShowConnectedNotificationChanged = {},
|
||||
onKeepNotificationAfterDisconnectChanged = {},
|
||||
onDebugSettings = {},
|
||||
onOffloadedFilteringDisabledChanged = {},
|
||||
onOffloadedBatchingDisabledChanged = {},
|
||||
onUseIndirectScanResultCallbackChanged = {},
|
||||
onHideUnmatchedDevicesChanged = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -345,60 +310,11 @@ private fun GeneralSettingsScreenNonProPreview() = PreviewWrapper {
|
||||
GeneralSettingsScreen(
|
||||
state = previewGeneralState(isPro = false),
|
||||
onNavigateUp = {},
|
||||
onMonitorModeSelected = {},
|
||||
onShowConnectedNotificationChanged = {},
|
||||
onKeepNotificationAfterDisconnectChanged = {},
|
||||
onDebugSettings = {},
|
||||
onOffloadedFilteringDisabledChanged = {},
|
||||
onOffloadedBatchingDisabledChanged = {},
|
||||
onUseIndirectScanResultCallbackChanged = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> ListPreferenceDialog(
|
||||
title: String,
|
||||
entries: List<T>,
|
||||
selectedEntry: T,
|
||||
onEntrySelected: (T) -> Unit,
|
||||
entryLabel: @Composable (T) -> String,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Column(Modifier.selectableGroup()) {
|
||||
entries.forEach { entry ->
|
||||
val isSelected = entry == selectedEntry
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { onEntrySelected(entry) },
|
||||
role = Role.RadioButton,
|
||||
)
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null,
|
||||
)
|
||||
Text(
|
||||
text = entryLabel(entry),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
onHideUnmatchedDevicesChanged = {},
|
||||
)
|
||||
}
|
||||
|
||||
+13
-19
@@ -13,7 +13,6 @@ import eu.darken.capod.common.theming.ThemeStyle
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.themeState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -30,12 +29,12 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
|
||||
data class State(
|
||||
val isPro: Boolean,
|
||||
val monitorMode: MonitorMode,
|
||||
val showConnectedNotification: Boolean,
|
||||
val keepNotificationAfterDisconnect: Boolean,
|
||||
val isOffloadedFilteringDisabled: Boolean,
|
||||
val isOffloadedBatchingDisabled: Boolean,
|
||||
val useIndirectScanResultCallback: Boolean,
|
||||
val hideUnmatchedDevices: Boolean,
|
||||
val themeState: ThemeState,
|
||||
)
|
||||
|
||||
@@ -43,12 +42,11 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
|
||||
val state = combine(
|
||||
combine(
|
||||
generalSettings.monitorMode.flow,
|
||||
generalSettings.useExtraMonitorNotification.flow,
|
||||
generalSettings.keepConnectedNotificationAfterDisconnect.flow,
|
||||
) { monitorMode, showNotif, keepNotif ->
|
||||
) { showNotif, keepNotif ->
|
||||
@Suppress("USELESS_CAST")
|
||||
arrayOf<Any>(monitorMode as Any, showNotif as Any, keepNotif as Any)
|
||||
arrayOf<Any>(showNotif as Any, keepNotif as Any)
|
||||
},
|
||||
combine(
|
||||
generalSettings.isOffloadedFilteringDisabled.flow,
|
||||
@@ -60,24 +58,20 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
},
|
||||
generalSettings.themeState,
|
||||
isPro,
|
||||
) { general, compat, themeState, isPro ->
|
||||
generalSettings.hideUnmatchedDevices.flow,
|
||||
) { general, compat, themeState, isPro, hideUnmatched ->
|
||||
State(
|
||||
isPro = isPro,
|
||||
monitorMode = general[0] as MonitorMode,
|
||||
showConnectedNotification = general[1] as Boolean,
|
||||
keepNotificationAfterDisconnect = general[2] as Boolean,
|
||||
showConnectedNotification = general[0] as Boolean,
|
||||
keepNotificationAfterDisconnect = general[1] as Boolean,
|
||||
isOffloadedFilteringDisabled = compat[0] as Boolean,
|
||||
isOffloadedBatchingDisabled = compat[1] as Boolean,
|
||||
useIndirectScanResultCallback = compat[2] as Boolean,
|
||||
hideUnmatchedDevices = hideUnmatched,
|
||||
themeState = themeState,
|
||||
)
|
||||
}.asLiveState()
|
||||
|
||||
fun setMonitorMode(mode: MonitorMode) {
|
||||
log(TAG, INFO) { "setMonitorMode($mode)" }
|
||||
generalSettings.monitorMode.valueBlocking = mode
|
||||
}
|
||||
|
||||
fun setShowConnectedNotification(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setShowConnectedNotification($enabled)" }
|
||||
generalSettings.useExtraMonitorNotification.valueBlocking = enabled
|
||||
@@ -103,6 +97,11 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = enabled
|
||||
}
|
||||
|
||||
fun setHideUnmatchedDevices(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setHideUnmatchedDevices($enabled)" }
|
||||
generalSettings.hideUnmatchedDevices.valueBlocking = enabled
|
||||
}
|
||||
|
||||
fun setThemeMode(mode: ThemeMode) = launch {
|
||||
log(TAG, INFO) { "setThemeMode($mode)" }
|
||||
if (isPro.first()) {
|
||||
@@ -135,11 +134,6 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
|
||||
fun goToDebugSettings() {
|
||||
log(TAG, INFO) { "goToDebugSettings()" }
|
||||
navTo(Nav.Settings.Debug)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Settings", "General", "VM")
|
||||
}
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package eu.darken.capod.main.ui.settings.general.debug
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.navigation.NavigationEntry
|
||||
import javax.inject.Inject
|
||||
|
||||
class DebugSettingsNavigation @Inject constructor() : NavigationEntry {
|
||||
override fun EntryProviderScope<NavKey>.setup() {
|
||||
entry<Nav.Settings.Debug> { DebugSettingsScreenHost() }
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class Mod {
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bind(entry: DebugSettingsNavigation): NavigationEntry
|
||||
}
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
package eu.darken.capod.main.ui.settings.general.debug
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
||||
import androidx.compose.material.icons.twotone.BugReport
|
||||
import androidx.compose.material.icons.twotone.DataArray
|
||||
import androidx.compose.material.icons.twotone.DevicesOther
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import eu.darken.capod.common.error.ErrorEventHandler
|
||||
import eu.darken.capod.common.navigation.NavigationEventHandler
|
||||
import eu.darken.capod.common.settings.SettingsBaseItem
|
||||
|
||||
@Composable
|
||||
fun DebugSettingsScreenHost(vm: DebugSettingsViewModel = hiltViewModel()) {
|
||||
ErrorEventHandler(vm)
|
||||
NavigationEventHandler(vm)
|
||||
|
||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||
state?.let {
|
||||
DebugSettingsScreen(
|
||||
state = it,
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onDebugModeChanged = { enabled -> vm.setDebugModeEnabled(enabled) },
|
||||
onShowFakeDataChanged = { enabled -> vm.setShowFakeData(enabled) },
|
||||
onShowUnfilteredChanged = { enabled -> vm.setShowUnfiltered(enabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DebugSettingsScreen(
|
||||
state: DebugSettingsViewModel.State,
|
||||
onNavigateUp: () -> Unit,
|
||||
onDebugModeChanged: (Boolean) -> Unit,
|
||||
onShowFakeDataChanged: (Boolean) -> Unit,
|
||||
onShowUnfilteredChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(R.string.settings_debug_label)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateUp) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
item {
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_debug_mode_label),
|
||||
subtitle = stringResource(R.string.settings_debug_mode_description),
|
||||
icon = Icons.TwoTone.BugReport,
|
||||
onClick = { onDebugModeChanged(!state.isDebugModeEnabled) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.isDebugModeEnabled,
|
||||
onCheckedChange = onDebugModeChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_fake_data_label),
|
||||
subtitle = stringResource(R.string.settings_fake_data_description),
|
||||
icon = Icons.TwoTone.DataArray,
|
||||
onClick = { onShowFakeDataChanged(!state.showFakeData) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.showFakeData,
|
||||
onCheckedChange = onShowFakeDataChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_blescanner_unfiltered_label),
|
||||
subtitle = stringResource(R.string.settings_blescanner_unfiltered_description),
|
||||
icon = Icons.TwoTone.DevicesOther,
|
||||
onClick = { onShowUnfilteredChanged(!state.showUnfiltered) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.showUnfiltered,
|
||||
onCheckedChange = onShowUnfilteredChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun DebugSettingsScreenPreview() = PreviewWrapper {
|
||||
DebugSettingsScreen(
|
||||
state = DebugSettingsViewModel.State(
|
||||
isDebugModeEnabled = true,
|
||||
showFakeData = false,
|
||||
showUnfiltered = false,
|
||||
),
|
||||
onNavigateUp = {},
|
||||
onDebugModeChanged = {},
|
||||
onShowFakeDataChanged = {},
|
||||
onShowUnfilteredChanged = {},
|
||||
)
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
package eu.darken.capod.main.ui.settings.general.debug
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
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.uix.ViewModel4
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import javax.inject.Inject
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@HiltViewModel
|
||||
class DebugSettingsViewModel @Inject constructor(
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
private val debugSettings: DebugSettings,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
data class State(
|
||||
val isDebugModeEnabled: Boolean,
|
||||
val showFakeData: Boolean,
|
||||
val showUnfiltered: Boolean,
|
||||
)
|
||||
|
||||
val state = combine(
|
||||
debugSettings.isDebugModeEnabled.flow,
|
||||
debugSettings.showFakeData.flow,
|
||||
debugSettings.showUnfiltered.flow,
|
||||
) { debugMode, fakeData, unfiltered ->
|
||||
State(
|
||||
isDebugModeEnabled = debugMode,
|
||||
showFakeData = fakeData,
|
||||
showUnfiltered = unfiltered,
|
||||
)
|
||||
}.asLiveState()
|
||||
|
||||
fun setDebugModeEnabled(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setDebugModeEnabled($enabled)" }
|
||||
debugSettings.isDebugModeEnabled.valueBlocking = enabled
|
||||
}
|
||||
|
||||
fun setShowFakeData(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setShowFakeData($enabled)" }
|
||||
debugSettings.showFakeData.valueBlocking = enabled
|
||||
}
|
||||
|
||||
fun setShowUnfiltered(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setShowUnfiltered($enabled)" }
|
||||
debugSettings.showUnfiltered.valueBlocking = enabled
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Settings", "Debug", "VM")
|
||||
}
|
||||
}
|
||||
@@ -58,13 +58,21 @@ object AncWidgetRenderStateMapper {
|
||||
primaryText = context.getString(R.string.anc_widget_no_anc_support_label),
|
||||
)
|
||||
|
||||
!device.isAapConnected -> AncWidgetRenderState.Message(
|
||||
theme = theme,
|
||||
resolvedBgColor = bgColor,
|
||||
resolvedTextColor = textColor,
|
||||
resolvedIconColor = iconColor,
|
||||
primaryText = context.getString(R.string.anc_widget_aap_not_connected_label),
|
||||
)
|
||||
!device.isAapConnected -> {
|
||||
val secondaryRes = if (device.ble != null) {
|
||||
R.string.anc_widget_aap_not_connected_nearby_description
|
||||
} else {
|
||||
R.string.anc_widget_aap_not_connected_not_nearby_description
|
||||
}
|
||||
AncWidgetRenderState.Message(
|
||||
theme = theme,
|
||||
resolvedBgColor = bgColor,
|
||||
resolvedTextColor = textColor,
|
||||
resolvedIconColor = iconColor,
|
||||
primaryText = profileLabel ?: device.getLabel(context),
|
||||
secondaryText = context.getString(secondaryRes),
|
||||
)
|
||||
}
|
||||
|
||||
!device.isAapReady -> AncWidgetRenderState.Message(
|
||||
theme = theme,
|
||||
|
||||
@@ -31,7 +31,7 @@ import androidx.compose.ui.unit.sp
|
||||
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.ble.toBatteryOrNull
|
||||
import eu.darken.capod.pods.core.apple.ble.isKnownBattery
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -160,7 +160,7 @@ private fun SinglePodPreview(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatPercent(state.percent.toBatteryOrNull()),
|
||||
text = formatPercent(state.percent),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
@@ -311,7 +311,7 @@ private fun PodItemRow(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatPercent(percent.toBatteryOrNull()),
|
||||
text = formatPercent(percent),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
@@ -354,7 +354,7 @@ private fun TinyPodItem(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatPercent(percent.toBatteryOrNull()),
|
||||
text = formatPercent(percent),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
@@ -382,9 +382,8 @@ private fun DeviceLabel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPercent(percent: Float?): String {
|
||||
return percent?.let { "${(it * 100).roundToInt()}%" } ?: "—"
|
||||
}
|
||||
private fun formatPercent(percent: Float): String =
|
||||
if (isKnownBattery(percent)) "${(percent * 100).roundToInt()}%" else "—"
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.apple.ble.toBatteryOrNull
|
||||
import eu.darken.capod.pods.core.apple.ble.isKnownBattery
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -122,7 +122,7 @@ private fun GlanceSinglePod(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatGlancePercent(state.percent.toBatteryOrNull()),
|
||||
text = formatGlancePercent(state.percent),
|
||||
style = textStyle,
|
||||
modifier = GlanceModifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
@@ -270,7 +270,7 @@ private fun GlancePodItem(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatGlancePercent(percent.toBatteryOrNull()),
|
||||
text = formatGlancePercent(percent),
|
||||
style = textStyle,
|
||||
modifier = GlanceModifier.padding(horizontal = 4.dp),
|
||||
)
|
||||
@@ -312,7 +312,7 @@ private fun GlanceTinyPodItem(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatGlancePercent(percent.toBatteryOrNull()),
|
||||
text = formatGlancePercent(percent),
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
modifier = GlanceModifier.padding(start = 4.dp),
|
||||
@@ -341,6 +341,5 @@ private fun GlanceDeviceLabel(
|
||||
|
||||
private fun fixedColor(argb: Int): ColorProvider = ColorProvider(Color(argb))
|
||||
|
||||
private fun formatGlancePercent(percent: Float?): String {
|
||||
return percent?.let { "${(it * 100).roundToInt()}%" } ?: "—"
|
||||
}
|
||||
private fun formatGlancePercent(percent: Float): String =
|
||||
if (isKnownBattery(percent)) "${(percent * 100).roundToInt()}%" else "—"
|
||||
|
||||
@@ -14,10 +14,10 @@ internal data class WidgetDeviceKey(
|
||||
val profileId: String?,
|
||||
val profileLabel: String?,
|
||||
val model: PodModel,
|
||||
val batteryLeft: Float?,
|
||||
val batteryRight: Float?,
|
||||
val batteryCase: Float?,
|
||||
val batteryHeadset: Float?,
|
||||
val batteryLeft: Float,
|
||||
val batteryRight: Float,
|
||||
val batteryCase: Float,
|
||||
val batteryHeadset: Float,
|
||||
val isLeftPodCharging: Boolean?,
|
||||
val isRightPodCharging: Boolean?,
|
||||
val isCaseCharging: Boolean?,
|
||||
@@ -27,6 +27,7 @@ internal data class WidgetDeviceKey(
|
||||
val isBeingWorn: Boolean?,
|
||||
val isAapConnected: Boolean,
|
||||
val isAapReady: Boolean,
|
||||
val hasBleAdvertisement: Boolean,
|
||||
val ancMode: AapSetting.AncMode.Value?,
|
||||
val pendingAncMode: AapSetting.AncMode.Value?,
|
||||
val visibleAncModes: List<AapSetting.AncMode.Value>,
|
||||
@@ -49,6 +50,7 @@ internal fun PodDevice.toWidgetKey(): WidgetDeviceKey = WidgetDeviceKey(
|
||||
isBeingWorn = isBeingWorn,
|
||||
isAapConnected = isAapConnected,
|
||||
isAapReady = isAapReady,
|
||||
hasBleAdvertisement = ble != null,
|
||||
ancMode = ancMode?.current,
|
||||
pendingAncMode = pendingAncMode,
|
||||
visibleAncModes = visibleAncModes,
|
||||
|
||||
@@ -7,7 +7,6 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ble.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.apple.ble.toBatteryFloat
|
||||
|
||||
object WidgetRenderStateMapper {
|
||||
|
||||
@@ -43,15 +42,15 @@ object WidgetRenderStateMapper {
|
||||
layout = layout,
|
||||
deviceLabel = profileLabel ?: device.getLabel(context),
|
||||
leftIcon = device.leftPodIcon,
|
||||
leftPercent = device.batteryLeft.toBatteryFloat(),
|
||||
leftPercent = device.batteryLeft,
|
||||
leftCharging = device.isLeftPodCharging == true,
|
||||
leftInEar = device.isLeftInEar == true,
|
||||
rightIcon = device.rightPodIcon,
|
||||
rightPercent = device.batteryRight.toBatteryFloat(),
|
||||
rightPercent = device.batteryRight,
|
||||
rightCharging = device.isRightPodCharging == true,
|
||||
rightInEar = device.isRightInEar == true,
|
||||
caseIcon = device.caseIcon,
|
||||
casePercent = device.batteryCase.toBatteryFloat(),
|
||||
casePercent = device.batteryCase,
|
||||
caseCharging = device.isCaseCharging == true,
|
||||
)
|
||||
|
||||
@@ -63,7 +62,7 @@ object WidgetRenderStateMapper {
|
||||
layout = layout,
|
||||
deviceLabel = profileLabel ?: device.getLabel(context),
|
||||
headsetIcon = device.iconRes,
|
||||
percent = device.batteryHeadset.toBatteryFloat(),
|
||||
percent = device.batteryHeadset,
|
||||
batteryIcon = getBatteryDrawable(device.batteryHeadset),
|
||||
charging = device.isHeadsetBeingCharged == true,
|
||||
worn = device.isBeingWorn == true,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import eu.darken.capod.common.bluetooth.NudgeAvailability
|
||||
import eu.darken.capod.common.bluetooth.NudgeCapabilityStore
|
||||
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.main.core.MonitorMode
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.toReactionConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MonitorModeResolver @Inject constructor(
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val nudgeCapabilityStore: NudgeCapabilityStore,
|
||||
) {
|
||||
|
||||
val effectiveMode: Flow<MonitorMode> = combine(
|
||||
profilesRepo.profiles,
|
||||
nudgeCapabilityStore.availability,
|
||||
) { profiles, nudge ->
|
||||
val primary = profiles.firstOrNull() ?: return@combine MonitorMode.MANUAL
|
||||
primary.requiredMode(nudge)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { log(TAG, VERBOSE) { "effectiveMode = $it" } }
|
||||
|
||||
private fun DeviceProfile.requiredMode(nudge: NudgeAvailability): MonitorMode = when {
|
||||
// Match AutoConnect.kt's isNullOrEmpty check — a blank/legacy "" address is no address.
|
||||
address.isNullOrBlank() -> MonitorMode.MANUAL
|
||||
toReactionConfig().autoConnect && nudge != NudgeAvailability.BROKEN -> MonitorMode.ALWAYS
|
||||
else -> MonitorMode.AUTOMATIC
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Monitor", "ModeResolver")
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import eu.darken.capod.pods.core.apple.ble.BATTERY_UNKNOWN
|
||||
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ble.SingleBlePodSnapshot
|
||||
@@ -138,18 +139,24 @@ data class PodDevice(
|
||||
}
|
||||
}
|
||||
|
||||
// Battery — AAP preferred, BLE fallback, then cached
|
||||
val batteryLeft: Float?
|
||||
get() = aap?.batteryLeft ?: (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent ?: cached?.left?.percent
|
||||
// Battery — AAP preferred, BLE fallback, then cached. Returns BATTERY_UNKNOWN (-1f)
|
||||
// for unknown to keep the type primitive; this is the boundary that previously emitted
|
||||
// Float? and triggered Android 10 ART JIT crashes via R8-merged unbox call sites.
|
||||
val batteryLeft: Float
|
||||
get() = aap?.batteryLeft ?: (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent
|
||||
?: cached?.left?.percent ?: BATTERY_UNKNOWN
|
||||
|
||||
val batteryRight: Float?
|
||||
get() = aap?.batteryRight ?: (ble as? DualBlePodSnapshot)?.batteryRightPodPercent ?: cached?.right?.percent
|
||||
val batteryRight: Float
|
||||
get() = aap?.batteryRight ?: (ble as? DualBlePodSnapshot)?.batteryRightPodPercent
|
||||
?: cached?.right?.percent ?: BATTERY_UNKNOWN
|
||||
|
||||
val batteryCase: Float?
|
||||
get() = aap?.batteryCase ?: (ble as? HasCase)?.batteryCasePercent ?: cached?.case?.percent
|
||||
val batteryCase: Float
|
||||
get() = aap?.batteryCase ?: (ble as? HasCase)?.batteryCasePercent
|
||||
?: cached?.case?.percent ?: BATTERY_UNKNOWN
|
||||
|
||||
val batteryHeadset: Float?
|
||||
get() = aap?.batteryHeadset ?: (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent ?: cached?.headset?.percent
|
||||
val batteryHeadset: Float
|
||||
get() = aap?.batteryHeadset ?: (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent
|
||||
?: cached?.headset?.percent ?: BATTERY_UNKNOWN
|
||||
|
||||
/** True when at least one displayed battery value was filled from cache (not live). */
|
||||
val isBatteryCached: Boolean
|
||||
@@ -260,6 +267,14 @@ data class PodDevice(
|
||||
val isEitherPodInEar: Boolean?
|
||||
get() = aap?.isEitherPodInEar ?: (ble as? HasEarDetectionDual)?.isEitherPodInEar
|
||||
|
||||
/**
|
||||
* True when ear detection comes from a live AAP reading. The BLE ear-detection bits are
|
||||
* unreliable for pods resting in the case (they report phantom "in ear" and disagree
|
||||
* between the two pods' advertisements), so consumers that must not act on a false "worn"
|
||||
* gate on this.
|
||||
*/
|
||||
val hasAapEarDetection: Boolean get() = aap?.aapEarDetection != null
|
||||
|
||||
val caseLidState: DualApplePods.LidState?
|
||||
get() = (ble as? DualApplePods)?.caseLidState
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class AapAutoConnect @Inject constructor(
|
||||
|
||||
log(TAG) { "AAP connecting to $address (${profile.label})" }
|
||||
try {
|
||||
aapManager.connect(address, bonded.internal, profile.model)
|
||||
aapManager.connect(address, bonded.internal!!, profile.model)
|
||||
log(TAG) { "AAP connected to $address" }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "AAP initial connect failed for $address: ${e.message}" }
|
||||
@@ -99,7 +99,7 @@ class AapAutoConnect @Inject constructor(
|
||||
|
||||
try {
|
||||
log(TAG) { "AAP initial retry ${attempt + 1} for $address after ${delayMs}ms" }
|
||||
aapManager.connect(address, bonded.internal, profile.model)
|
||||
aapManager.connect(address, bonded.internal!!, profile.model)
|
||||
log(TAG) { "AAP connected to $address on retry ${attempt + 1}" }
|
||||
break
|
||||
} catch (retryException: Exception) {
|
||||
@@ -159,7 +159,7 @@ class AapAutoConnect @Inject constructor(
|
||||
|
||||
try {
|
||||
log(TAG) { "AAP reconnect attempt ${attempt + 1} for $address in ${delayMs}ms" }
|
||||
aapManager.connect(address, bonded.internal, profile.model)
|
||||
aapManager.connect(address, bonded.internal!!, profile.model)
|
||||
log(TAG) { "AAP reconnected to $address" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
@@ -211,7 +211,7 @@ class AapAutoConnect @Inject constructor(
|
||||
val bonded = bluetoothManager.bondedDevices().first()
|
||||
.firstOrNull { it.address == address }
|
||||
if (bonded != null) {
|
||||
aapManager.connect(address, bonded.internal, detectedModel)
|
||||
aapManager.connect(address, bonded.internal!!, detectedModel)
|
||||
log(TAG) { "AAP reconnected $address with corrected model $detectedModel" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -66,7 +66,7 @@ class StemPressReaction @Inject constructor(
|
||||
is StemAction.PreviousTrack -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS)
|
||||
is StemAction.VolumeUp -> mediaControl.adjustVolumeUp()
|
||||
is StemAction.VolumeDown -> mediaControl.adjustVolumeDown()
|
||||
is StemAction.Stop -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_STOP)
|
||||
is StemAction.Stop -> mediaControl.sendStop()
|
||||
is StemAction.FastForward -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)
|
||||
is StemAction.Rewind -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_REWIND)
|
||||
is StemAction.MuteToggle -> mediaControl.toggleMuteMusic()
|
||||
|
||||
@@ -8,7 +8,6 @@ import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.bluetooth.onlyNewAndUnique
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -52,7 +52,6 @@ class BlePodMonitor @Inject constructor(
|
||||
private val timeSource: TimeSource,
|
||||
private val generalSettings: GeneralSettings,
|
||||
bluetoothManager: BluetoothManager2,
|
||||
private val debugSettings: DebugSettings,
|
||||
private val permissionTool: PermissionTool,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
) {
|
||||
@@ -60,6 +59,62 @@ class BlePodMonitor @Inject constructor(
|
||||
private val deviceCache = mutableMapOf<BlePodSnapshot.Id, BlePodSnapshot>()
|
||||
private val cacheLock = Mutex()
|
||||
|
||||
/**
|
||||
* Drops all cached observations. The troubleshooter calls this between probe attempts so a
|
||||
* previous compat combo's cached devices (kept up to [STALE_DEVICE_TIMEOUT]) can't leak into
|
||||
* the next attempt and falsely satisfy it — including via [preferCaseContextPod], which would
|
||||
* otherwise hand back a stale snapshot whose timestamp predates the fresh scan.
|
||||
*/
|
||||
suspend fun clearDeviceCache() = cacheLock.withLock {
|
||||
log(TAG) { "clearDeviceCache()" }
|
||||
deviceCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ephemeral override that disables the proximity-pairing scan filter so
|
||||
* the troubleshooter can collect raw BLE broadcasts. Resets to false on
|
||||
* every process start; the troubleshooter is the only writer.
|
||||
*/
|
||||
private val unfilteredOverride = MutableStateFlow(false)
|
||||
|
||||
fun setUnfilteredOverride(enabled: Boolean) {
|
||||
unfilteredOverride.value = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Ephemeral override for the three BLE compatibility options. The troubleshooter uses this to
|
||||
* probe combinations without writing the user's persisted settings: while set, it fully replaces
|
||||
* the persisted compat values for the active scan. Resets to null on every process start; the
|
||||
* troubleshooter is the only writer and always clears it when finished, so clearing it restores
|
||||
* the user's original settings for free.
|
||||
*/
|
||||
private val compatOverride = MutableStateFlow<CompatOverride?>(null)
|
||||
|
||||
fun setCompatOverride(override: CompatOverride?) {
|
||||
log(TAG) { "setCompatOverride($override)" }
|
||||
compatOverride.value = override
|
||||
}
|
||||
|
||||
data class CompatOverride(
|
||||
val offloadedFilteringDisabled: Boolean,
|
||||
val offloadedBatchingDisabled: Boolean,
|
||||
val indirectCallback: Boolean,
|
||||
)
|
||||
|
||||
/** Persisted compat settings, transparently replaced by [compatOverride] while it is set. */
|
||||
private val effectiveCompat: Flow<CompatOverride> = combine(
|
||||
compatOverride,
|
||||
generalSettings.isOffloadedFilteringDisabled.flow,
|
||||
generalSettings.isOffloadedBatchingDisabled.flow,
|
||||
generalSettings.useIndirectScanResultCallback.flow,
|
||||
) { override, filteringDisabled, batchingDisabled, indirectCallback ->
|
||||
override ?: CompatOverride(
|
||||
offloadedFilteringDisabled = filteringDisabled,
|
||||
offloadedBatchingDisabled = batchingDisabled,
|
||||
indirectCallback = indirectCallback,
|
||||
)
|
||||
}
|
||||
|
||||
val devices: Flow<List<BlePodSnapshot>> = combine(
|
||||
permissionTool.missingScanPermissions,
|
||||
bluetoothManager.isBluetoothEnabled
|
||||
@@ -134,23 +189,15 @@ class BlePodMonitor @Inject constructor(
|
||||
|
||||
private fun createBleScanner() = combine(
|
||||
bleScanModeController.scannerMode,
|
||||
debugSettings.showUnfiltered.flow,
|
||||
generalSettings.isOffloadedBatchingDisabled.flow,
|
||||
generalSettings.isOffloadedFilteringDisabled.flow,
|
||||
generalSettings.useIndirectScanResultCallback.flow,
|
||||
) {
|
||||
scannermode,
|
||||
showUnfiltered,
|
||||
isOffloadedBatchingDisabled,
|
||||
isOffloadedFilteringDisabled,
|
||||
useIndirectScanResultCallback,
|
||||
->
|
||||
unfilteredOverride,
|
||||
effectiveCompat,
|
||||
) { scannermode, showUnfiltered, compat ->
|
||||
ScannerOptions(
|
||||
scannerMode = scannermode,
|
||||
showUnfiltered = showUnfiltered,
|
||||
offloadedFilteringDisabled = isOffloadedFilteringDisabled,
|
||||
offloadedBatchingDisabled = isOffloadedBatchingDisabled,
|
||||
disableDirectCallback = useIndirectScanResultCallback,
|
||||
offloadedFilteringDisabled = compat.offloadedFilteringDisabled,
|
||||
offloadedBatchingDisabled = compat.offloadedBatchingDisabled,
|
||||
disableDirectCallback = compat.indirectCallback,
|
||||
)
|
||||
}
|
||||
.throttleLatest(1000)
|
||||
|
||||
+15
-12
@@ -3,9 +3,11 @@ package eu.darken.capod.monitor.core.cache
|
||||
import eu.darken.capod.common.SystemTimeSource
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.cache.CachedDeviceState.CachedBatterySlot
|
||||
import eu.darken.capod.pods.core.apple.ble.BATTERY_UNKNOWN
|
||||
import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ble.SingleBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.HasCase
|
||||
import eu.darken.capod.pods.core.apple.ble.isKnownBattery
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -24,15 +26,17 @@ fun PodDevice.toCachedState(
|
||||
if (!isLive) return null
|
||||
val pid = profileId ?: return null
|
||||
|
||||
val liveLeft = aap?.batteryLeft ?: (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent
|
||||
val liveRight = aap?.batteryRight ?: (ble as? DualBlePodSnapshot)?.batteryRightPodPercent
|
||||
val liveCase = aap?.batteryCase ?: (ble as? HasCase)?.batteryCasePercent
|
||||
val liveHeadset = aap?.batteryHeadset ?: (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent
|
||||
// RAW LIVE EXTRACTION — must NOT use device.batteryLeft etc. (which fall back to cache).
|
||||
// Reading the unified getter would re-stamp stale cached readings as fresh live data.
|
||||
val liveLeft = aap?.batteryLeft ?: (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent ?: BATTERY_UNKNOWN
|
||||
val liveRight = aap?.batteryRight ?: (ble as? DualBlePodSnapshot)?.batteryRightPodPercent ?: BATTERY_UNKNOWN
|
||||
val liveCase = aap?.batteryCase ?: (ble as? HasCase)?.batteryCasePercent ?: BATTERY_UNKNOWN
|
||||
val liveHeadset = aap?.batteryHeadset ?: (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent ?: BATTERY_UNKNOWN
|
||||
val liveDeviceInfo = aap?.deviceInfo
|
||||
|
||||
if (liveLeft == null && liveRight == null && liveCase == null && liveHeadset == null && liveDeviceInfo == null) {
|
||||
return null
|
||||
}
|
||||
if (!isKnownBattery(liveLeft) && !isKnownBattery(liveRight) &&
|
||||
!isKnownBattery(liveCase) && !isKnownBattery(liveHeadset) && liveDeviceInfo == null
|
||||
) return null
|
||||
|
||||
val newState = CachedDeviceState(
|
||||
profileId = pid,
|
||||
@@ -61,15 +65,14 @@ fun PodDevice.toCachedState(
|
||||
}
|
||||
|
||||
private fun mergeBatterySlot(
|
||||
livePercent: Float?,
|
||||
livePercent: Float,
|
||||
existing: CachedBatterySlot?,
|
||||
now: Instant,
|
||||
): CachedBatterySlot? {
|
||||
val live: Float = livePercent ?: return existing
|
||||
val current: CachedBatterySlot = existing ?: return CachedBatterySlot(live, now)
|
||||
|
||||
if (!isKnownBattery(livePercent)) return existing
|
||||
val current = existing ?: return CachedBatterySlot(livePercent, now)
|
||||
val isStale = Duration.between(current.updatedAt, now).abs() > Duration.ofMinutes(1)
|
||||
return if (current.percent == live && !isStale) current else CachedBatterySlot(live, now)
|
||||
return if (current.percent == livePercent && !isStale) current else CachedBatterySlot(livePercent, now)
|
||||
}
|
||||
|
||||
private fun hasStateChanged(old: CachedDeviceState, new: CachedDeviceState): Boolean {
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
@@ -30,6 +29,7 @@ import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.MonitorModeResolver
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.ble.BlePodMonitor
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
@@ -42,6 +42,9 @@ 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.conversation.ConversationReaction
|
||||
import eu.darken.capod.reaction.core.charged.ChargedReaction
|
||||
import eu.darken.capod.reaction.core.charged.ChargedReactionNotifications
|
||||
import eu.darken.capod.reaction.core.sleep.SleepReaction
|
||||
import eu.darken.capod.reaction.ui.popup.PopUpWindow
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -78,9 +81,13 @@ class MonitorService : Service() {
|
||||
@Inject lateinit var autoConnect: AutoConnect
|
||||
@Inject lateinit var popUpReaction: PopUpReaction
|
||||
@Inject lateinit var sleepReaction: SleepReaction
|
||||
@Inject lateinit var chargedReaction: ChargedReaction
|
||||
@Inject lateinit var chargedReactionNotifications: ChargedReactionNotifications
|
||||
@Inject lateinit var conversationReaction: ConversationReaction
|
||||
@Inject lateinit var popUpWindow: PopUpWindow
|
||||
@Inject lateinit var profilesRepo: DeviceProfilesRepo
|
||||
@Inject lateinit var aapConnectionManager: AapConnectionManager
|
||||
@Inject lateinit var monitorModeResolver: MonitorModeResolver
|
||||
|
||||
private val monitorScope = MonitorCoroutineScope()
|
||||
private var monitoringJob: Job? = null
|
||||
@@ -88,6 +95,10 @@ class MonitorService : Service() {
|
||||
private var foregroundStartFailed = false
|
||||
private var injectionComplete = false
|
||||
|
||||
@Volatile
|
||||
private var latestNotificationSettings: NotificationSettings =
|
||||
NotificationSettings(useExtraNotification = false, keepAfterDisconnect = false)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun promoteToForeground(notification: Notification): Boolean {
|
||||
return try {
|
||||
@@ -188,27 +199,53 @@ class MonitorService : Service() {
|
||||
}
|
||||
|
||||
private suspend fun doMonitor() {
|
||||
// Seed snapshot eagerly so onDestroy() has correct values even if we abort below.
|
||||
latestNotificationSettings = NotificationSettings(
|
||||
useExtraNotification = generalSettings.useExtraMonitorNotification.flow.first(),
|
||||
keepAfterDisconnect = generalSettings.keepConnectedNotificationAfterDisconnect.flow.first(),
|
||||
)
|
||||
|
||||
val permissionsMissingOnStart = permissionTool.missingScanPermissions.first()
|
||||
if (permissionsMissingOnStart.isNotEmpty()) {
|
||||
log(TAG, WARN) { "Aborting, missing scan permissions: $permissionsMissingOnStart" }
|
||||
return
|
||||
}
|
||||
|
||||
val monitorJob = deviceMonitor.primaryDevice()
|
||||
val deviceFlow = deviceMonitor.primaryDevice()
|
||||
.setupCommonEventHandlers(TAG) { "BlePodMonitor" }
|
||||
.distinctUntilChangedBy { it?.toNotificationKey() }
|
||||
.throttleLatest(1000)
|
||||
.onEach { currentDevice ->
|
||||
val useExtraNotification = generalSettings.useExtraMonitorNotification.valueBlocking
|
||||
|
||||
val notificationSettingsFlow = combine(
|
||||
generalSettings.useExtraMonitorNotification.flow,
|
||||
generalSettings.keepConnectedNotificationAfterDisconnect.flow,
|
||||
) { useExtra, keepAfter ->
|
||||
NotificationSettings(useExtraNotification = useExtra, keepAfterDisconnect = keepAfter)
|
||||
}
|
||||
|
||||
val monitorJob = combine(deviceFlow, notificationSettingsFlow) { currentDevice, settings ->
|
||||
currentDevice to settings
|
||||
}
|
||||
.onEach { (currentDevice, settings) ->
|
||||
latestNotificationSettings = settings
|
||||
|
||||
notificationManager.notify(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
notifications.getNotification(currentDevice, showHint = useExtraNotification),
|
||||
notifications.getNotification(
|
||||
currentDevice,
|
||||
showHint = settings.useExtraNotification,
|
||||
),
|
||||
)
|
||||
if (generalSettings.useExtraMonitorNotification.valueBlocking && currentDevice != null) {
|
||||
notificationManager.notify(
|
||||
|
||||
when (val action = decideExtraNotificationAction(currentDevice, settings)) {
|
||||
is ExtraNotificationAction.Post -> notificationManager.notify(
|
||||
MonitorNotifications.NOTIFICATION_ID_CONNECTED,
|
||||
notifications.getNotificationConnected(currentDevice),
|
||||
notifications.getNotificationConnected(action.device),
|
||||
)
|
||||
ExtraNotificationAction.Cancel -> notificationManager.cancel(
|
||||
MonitorNotifications.NOTIFICATION_ID_CONNECTED
|
||||
)
|
||||
ExtraNotificationAction.KeepExisting -> Unit
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
@@ -224,7 +261,7 @@ class MonitorService : Service() {
|
||||
emptyFlow()
|
||||
} else {
|
||||
combine(
|
||||
generalSettings.monitorMode.flow,
|
||||
monitorModeResolver.effectiveMode,
|
||||
profilesRepo.profiles,
|
||||
bluetoothManager.connectedDevices,
|
||||
aapConnectionManager.allStates,
|
||||
@@ -300,6 +337,28 @@ class MonitorService : Service() {
|
||||
.catch { log(TAG, WARN) { "sleepReaction failed:\n${it.asLog()}" } }
|
||||
.launchIn(monitorScope)
|
||||
|
||||
chargedReaction.monitor()
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is ChargedReaction.Event.ShowNotification -> chargedReactionNotifications.show(
|
||||
profileId = event.profileId,
|
||||
deviceLabel = event.deviceLabel,
|
||||
thresholdPercent = event.thresholdPercent,
|
||||
)
|
||||
|
||||
is ChargedReaction.Event.CancelNotification ->
|
||||
chargedReactionNotifications.cancel(event.profileId)
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "chargedReaction" }
|
||||
.catch { log(TAG, WARN) { "chargedReaction failed:\n${it.asLog()}" } }
|
||||
.launchIn(monitorScope)
|
||||
|
||||
conversationReaction.monitor()
|
||||
.setupCommonEventHandlers(TAG) { "conversationReaction" }
|
||||
.catch { log(TAG, WARN) { "conversationReaction failed:\n${it.asLog()}" } }
|
||||
.launchIn(monitorScope)
|
||||
|
||||
log(TAG, VERBOSE) { "Monitor job is active" }
|
||||
monitorJob.join()
|
||||
log(TAG, VERBOSE) { "Monitor job quit" }
|
||||
@@ -310,7 +369,13 @@ class MonitorService : Service() {
|
||||
monitorScope.cancel("Service destroyed")
|
||||
|
||||
if (injectionComplete) {
|
||||
if (generalSettings.useExtraMonitorNotification.valueBlocking && !generalSettings.keepConnectedNotificationAfterDisconnect.valueBlocking) {
|
||||
try {
|
||||
chargedReactionNotifications.cancelAll()
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "Failed to cancel charged notifications: ${e.message}" }
|
||||
}
|
||||
val snapshot = latestNotificationSettings
|
||||
if (snapshot.useExtraNotification && !snapshot.keepAfterDisconnect) {
|
||||
try {
|
||||
notificationManager.cancel(MonitorNotifications.NOTIFICATION_ID_CONNECTED)
|
||||
} catch (e: Exception) {
|
||||
@@ -363,6 +428,7 @@ private data class NotificationDeviceKey(
|
||||
val profileId: String?,
|
||||
val label: String?,
|
||||
val model: PodModel,
|
||||
val isLive: Boolean,
|
||||
val hasDualPods: Boolean,
|
||||
val hasCase: Boolean,
|
||||
val hasEarDetection: Boolean,
|
||||
@@ -387,6 +453,7 @@ private fun PodDevice.toNotificationKey(): NotificationDeviceKey = NotificationD
|
||||
profileId = profileId,
|
||||
label = label,
|
||||
model = model,
|
||||
isLive = isLive,
|
||||
hasDualPods = hasDualPods,
|
||||
hasCase = hasCase,
|
||||
hasEarDetection = hasEarDetection,
|
||||
@@ -406,3 +473,24 @@ private fun PodDevice.toNotificationKey(): NotificationDeviceKey = NotificationD
|
||||
rightPodIcon = rightPodIcon,
|
||||
caseIcon = caseIcon,
|
||||
)
|
||||
|
||||
internal data class NotificationSettings(
|
||||
val useExtraNotification: Boolean,
|
||||
val keepAfterDisconnect: Boolean,
|
||||
)
|
||||
|
||||
internal sealed interface ExtraNotificationAction {
|
||||
object Cancel : ExtraNotificationAction
|
||||
data class Post(val device: PodDevice) : ExtraNotificationAction
|
||||
object KeepExisting : ExtraNotificationAction
|
||||
}
|
||||
|
||||
internal fun decideExtraNotificationAction(
|
||||
device: PodDevice?,
|
||||
settings: NotificationSettings,
|
||||
): ExtraNotificationAction = when {
|
||||
!settings.useExtraNotification -> ExtraNotificationAction.Cancel
|
||||
device != null && device.isLive -> ExtraNotificationAction.Post(device)
|
||||
settings.keepAfterDisconnect -> ExtraNotificationAction.KeepExisting
|
||||
else -> ExtraNotificationAction.Cancel
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.apple.ble.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.apple.ble.isKnownBattery
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -150,9 +151,7 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
setTextViewText(R.id.device, device.getLabel(context))
|
||||
}
|
||||
|
||||
private fun percentToInt(percent: Float?): Int {
|
||||
if (percent == null) return 0
|
||||
return (percent * 100).roundToInt().coerceIn(0, 100)
|
||||
}
|
||||
private fun percentToInt(percent: Float): Int =
|
||||
if (isKnownBattery(percent)) (percent * 100).roundToInt().coerceIn(0, 100) else 0
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.engine.AapConnection
|
||||
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.ConversationAwarenessEvent
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -73,6 +74,17 @@ class AapConnectionManager @Inject constructor(
|
||||
private val _sleepEvents = MutableSharedFlow<BluetoothAddress>(extraBufferCapacity = 16)
|
||||
val sleepEvents: SharedFlow<BluetoothAddress> = _sleepEvents.asSharedFlow()
|
||||
|
||||
/**
|
||||
* Emits when a connected device reports a Conversational Awareness speaking transition
|
||||
* (START/STOP, AAP command 0x4B). Paired with the origin address so the conversation
|
||||
* reaction can gate on the primary device. Only the known 0x01/0x04 markers reach here —
|
||||
* the engine drops unknown raw values (see [AapSessionEngine]).
|
||||
*/
|
||||
private val _conversationalAwarenessEvents =
|
||||
MutableSharedFlow<Pair<BluetoothAddress, ConversationAwarenessEvent>>(extraBufferCapacity = 16)
|
||||
val conversationalAwarenessEvents: SharedFlow<Pair<BluetoothAddress, ConversationAwarenessEvent>> =
|
||||
_conversationalAwarenessEvents.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()
|
||||
@@ -140,6 +152,13 @@ class AapConnectionManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Forward conversational-awareness speaking transitions from this connection (child coroutine).
|
||||
launch {
|
||||
connection.conversationalAwarenessEvents.collect { event ->
|
||||
_conversationalAwarenessEvents.tryEmit(address to event)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward OFF-rejection events from this connection (child coroutine)
|
||||
launch {
|
||||
connection.offRejected.collect {
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.AapPacket
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSleepEvent
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.ConversationAwarenessEvent
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -53,6 +54,7 @@ internal class AapConnection(
|
||||
val keysReceived: SharedFlow<KeyExchangeResult> get() = engine.keysReceived
|
||||
val stemPressEvents: SharedFlow<StemPressEvent> get() = engine.stemPressEvents
|
||||
val sleepEvents: SharedFlow<AapSleepEvent> get() = engine.sleepEvents
|
||||
val conversationalAwarenessEvents: SharedFlow<ConversationAwarenessEvent> get() = engine.conversationalAwarenessEvents
|
||||
val offRejected: SharedFlow<Unit> get() = engine.offRejected
|
||||
val settingRejected: SharedFlow<AapCommand> get() = engine.settingRejected
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.ConversationAwarenessEvent
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -54,6 +55,11 @@ internal class AapSessionEngine(
|
||||
MutableSharedFlow<AapSleepEvent>(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val sleepEvents: SharedFlow<AapSleepEvent> = _sleepEvents.asSharedFlow()
|
||||
|
||||
private val _conversationalAwarenessEvents =
|
||||
MutableSharedFlow<ConversationAwarenessEvent>(extraBufferCapacity = 8, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val conversationalAwarenessEvents: SharedFlow<ConversationAwarenessEvent> =
|
||||
_conversationalAwarenessEvents.asSharedFlow()
|
||||
|
||||
private val _offRejected =
|
||||
MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val offRejected: SharedFlow<Unit> = _offRejected.asSharedFlow()
|
||||
@@ -301,6 +307,13 @@ internal class AapSessionEngine(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-emit every (well-formed) Conversational Awareness frame as a classified event for the
|
||||
// conversation reaction: START / STOP / HOLD (keep-alive). The decoder already dropped
|
||||
// malformed frames (rawValue stays null only in that case). The raw payload remains logged.
|
||||
if (value is AapSetting.ConversationalAwarenessState) {
|
||||
value.rawValue?.let { _conversationalAwarenessEvents.tryEmit(ConversationAwarenessEvent.fromStatus(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTimerFired(key: EngineTimerKey) {
|
||||
|
||||
@@ -99,9 +99,17 @@ sealed class AapSetting {
|
||||
val level: Int,
|
||||
) : AapSetting()
|
||||
|
||||
/** Push-only from device — reports speaking detection state (command 0x4B). */
|
||||
/**
|
||||
* Push-only from device — reports speaking detection state (command 0x4B).
|
||||
*
|
||||
* [rawValue] is the status byte: the last byte of the 4-byte `02 00 01 XX` form (or the single
|
||||
* byte of the legacy form), preserved so consumers can classify it. [speaking] is `true` only
|
||||
* for the speaking-onset statuses (`1`, `2`); every other value (`0`, `3`, `4`, `5`, `0x0B`, …)
|
||||
* is `false`. START/STOP/HOLD classification for the reaction lives in [ConversationAwarenessEvent].
|
||||
*/
|
||||
data class ConversationalAwarenessState(
|
||||
val speaking: Boolean,
|
||||
val rawValue: Int? = null,
|
||||
) : AapSetting()
|
||||
|
||||
data class MicrophoneMode(
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package eu.darken.capod.pods.core.apple.aap.protocol
|
||||
|
||||
/**
|
||||
* Classified Conversational Awareness signal derived from the status byte of a `0x4B` frame.
|
||||
*
|
||||
* Status-byte mapping (from live AirPods Pro 3 + Pro 2 USB-C captures — both models share one
|
||||
* firmware train and a byte-identical protocol — plus the librepods project):
|
||||
* - `1`, `2` → [START] (wearer started / is speaking → engage the reaction)
|
||||
* - `5`, `6`, `8`, `9` → [STOP] (wearer stopped → disengage). All four confirmed live on Pro 2
|
||||
* USB-C fw `…6814`; which one terminates a given flurry varies with how speech ended.
|
||||
* - any other value (`0`, `3`, `4`, `7`, `0x0B`, … and anything unrecognised) → [HOLD]: a
|
||||
* transitional wind-down frame (`7` was only discovered on fw `…6814` — the set is open-ended,
|
||||
* so unknown values are deliberately classified as HOLD rather than guessed at).
|
||||
*
|
||||
* The pod sends NO frames during active speech — it stays engaged (and silent) for as long as it
|
||||
* hears nearby voices, 20-30s+ observed. So frame-silence must NOT be read as "speaking ended".
|
||||
* Conversely, any non-START frame means the wind-down has begun: a short flurry of transitional
|
||||
* and terminal frames (e.g. `3,0xB,4,8,9` or `3,5,7,8,9`). With only ONE pod worn (other in
|
||||
* case/disconnected) the terminal is deterministically dropped — the flurry ends on a transitional
|
||||
* `4` (#608; reproduced on Pro 3 and Pro 2 alike) — so [ConversationReaction] treats a HOLD as
|
||||
* "terminal imminent" and arms a short fuse, with a long stale backstop for a fully-dropped flurry.
|
||||
* A flurry's trailing frames may arrive after its terminal; they are ignored once disengaged.
|
||||
*/
|
||||
enum class ConversationAwarenessEvent {
|
||||
START,
|
||||
HOLD,
|
||||
STOP,
|
||||
;
|
||||
|
||||
companion object {
|
||||
val SPEAKING_STATUSES = setOf(1, 2)
|
||||
val STOPPED_STATUSES = setOf(5, 6, 8, 9)
|
||||
|
||||
fun fromStatus(status: Int): ConversationAwarenessEvent = when (status) {
|
||||
in SPEAKING_STATUSES -> START
|
||||
in STOPPED_STATUSES -> STOP
|
||||
else -> HOLD
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-5
@@ -183,12 +183,24 @@ class DefaultAapDeviceProfile(
|
||||
return AapSetting.PmeConfig::class to AapSetting.PmeConfig(sets)
|
||||
}
|
||||
|
||||
// Conversation Awareness State is a separate command type (push-only)
|
||||
// Conversation Awareness State is a separate command type (push-only). Two payload shapes
|
||||
// are seen: a legacy single byte, and the 4-byte `02 00 01 <status>` form on Pro 3 fw 81.x.
|
||||
// The status (speaking marker) is the last byte in both — but we validate the shape rather
|
||||
// than blindly taking payload.last(), so a truncated/garbled frame can't be misread as a
|
||||
// speaking transition. Unrecognised shapes return null → logged as an unhandled message.
|
||||
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)
|
||||
val p = message.payload
|
||||
val status = when {
|
||||
p.size == 1 -> p[0].toInt() and 0xFF
|
||||
p.size == 4 &&
|
||||
p[0] == 0x02.toByte() && p[1] == 0x00.toByte() && p[2] == 0x01.toByte() ->
|
||||
p[3].toInt() and 0xFF
|
||||
else -> return null
|
||||
}
|
||||
// speaking = the "started/active speaking" statuses (1,2); see ConversationAwarenessEvent.
|
||||
val speaking = status in ConversationAwarenessEvent.SPEAKING_STATUSES
|
||||
return AapSetting.ConversationalAwarenessState::class to
|
||||
AapSetting.ConversationalAwarenessState(speaking, rawValue = status)
|
||||
}
|
||||
|
||||
if (message.commandType != AapMessageType.CONTROL.value) return null
|
||||
|
||||
@@ -21,17 +21,18 @@ import kotlin.math.roundToInt
|
||||
|
||||
const val BATTERY_UNKNOWN = -1f
|
||||
|
||||
fun Float?.toBatteryFloat(): Float = this ?: BATTERY_UNKNOWN
|
||||
fun isKnownBattery(percent: Float): Boolean = percent.isFinite() && percent >= 0f
|
||||
|
||||
fun Float.toBatteryOrNull(): Float? = takeIf { it >= 0f }
|
||||
fun batteryProgress(percent: Float): Float =
|
||||
if (isKnownBattery(percent)) percent.coerceIn(0f, 1f) else 0f
|
||||
|
||||
fun formatBatteryPercent(context: Context, percent: Float?): String =
|
||||
percent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
fun formatBatteryPercent(context: Context, percent: Float): String =
|
||||
if (isKnownBattery(percent)) "${(percent * 100).roundToInt()}%"
|
||||
else context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
@DrawableRes
|
||||
fun getBatteryDrawable(percent: Float?): Int = when {
|
||||
percent == null -> R.drawable.ic_baseline_battery_unknown_24
|
||||
fun getBatteryDrawable(percent: Float): Int = when {
|
||||
!isKnownBattery(percent) -> R.drawable.ic_baseline_battery_unknown_24
|
||||
percent > 0.95f -> R.drawable.ic_baseline_battery_full_24
|
||||
percent > 0.80f -> R.drawable.ic_baseline_battery_6_bar_24
|
||||
percent > 0.65f -> R.drawable.ic_baseline_battery_5_bar_24
|
||||
@@ -42,8 +43,8 @@ fun getBatteryDrawable(percent: Float?): Int = when {
|
||||
else -> R.drawable.ic_baseline_battery_0_bar_24
|
||||
}
|
||||
|
||||
fun getBatteryIcon(percent: Float?): ImageVector = when {
|
||||
percent == null -> Icons.AutoMirrored.TwoTone.BatteryUnknown
|
||||
fun getBatteryIcon(percent: Float): ImageVector = when {
|
||||
!isKnownBattery(percent) -> Icons.AutoMirrored.TwoTone.BatteryUnknown
|
||||
percent > 0.95f -> Icons.TwoTone.BatteryFull
|
||||
percent > 0.80f -> Icons.TwoTone.Battery6Bar
|
||||
percent > 0.65f -> Icons.TwoTone.Battery5Bar
|
||||
|
||||
@@ -4,6 +4,14 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.pods.core.apple.ble.history.KnownDevice
|
||||
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPayload
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* How far back to look when recovering the lid state from a recent in-case-pod broadcast.
|
||||
* Time-bounded rather than count-bounded: BLE scan batching means a fixed number of frames is not a
|
||||
* stable time window, and an old reading must not be allowed to resurrect a stale OPEN/CLOSED.
|
||||
*/
|
||||
private val MAX_LID_RECOVERY_AGE: Duration = Duration.ofSeconds(2)
|
||||
|
||||
interface ApplePodsFactory {
|
||||
fun isResponsible(message: ProximityMessage): Boolean
|
||||
@@ -22,7 +30,8 @@ interface ApplePodsFactory {
|
||||
fun KnownDevice.getLatestCaseBattery(): Float? = this.lastCaseBattery
|
||||
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||
// A pod broadcasting from inside the case has authoritative case state
|
||||
// A pod broadcasting from inside the case has authoritative case state. Out-of-case frames
|
||||
// (one pod removed) report UNKNOWN via DualApplePods.caseLidState and are skipped here.
|
||||
if (basic.hasCaseContext && basic.caseLidState in setOf(
|
||||
DualApplePods.LidState.OPEN,
|
||||
DualApplePods.LidState.CLOSED,
|
||||
@@ -31,11 +40,14 @@ interface ApplePodsFactory {
|
||||
return basic.caseLidState
|
||||
}
|
||||
|
||||
// Current pod lacks case context (e.g. pod on desk) or reports UNKNOWN.
|
||||
// Check recent history for a sibling broadcast that has case context.
|
||||
val fromCaseContext = history
|
||||
.takeLast(4)
|
||||
// Current pod lacks case context (e.g. pod on desk) or reports UNKNOWN (out-of-case pod's
|
||||
// stale lid byte). Recover the last authoritative reading from a recent in-case broadcast,
|
||||
// bounded by time so a missed CLOSED can't keep a stale OPEN alive across scan gaps.
|
||||
val recentHistory = history
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.filter { Duration.between(it.seenLastAt, basic.seenLastAt).abs() <= MAX_LID_RECOVERY_AGE }
|
||||
|
||||
val fromCaseContext = recentHistory
|
||||
.lastOrNull { it.hasCaseContext && it.caseLidState != DualApplePods.LidState.UNKNOWN }
|
||||
?.caseLidState
|
||||
|
||||
@@ -44,10 +56,8 @@ interface ApplePodsFactory {
|
||||
// No case-context broadcast in recent history — current value is best we have
|
||||
if (basic.caseLidState != DualApplePods.LidState.UNKNOWN) return basic.caseLidState
|
||||
|
||||
// Last resort: any non-UNKNOWN from history
|
||||
return history
|
||||
.takeLast(2)
|
||||
.filterIsInstance<DualApplePods>()
|
||||
// Last resort: any non-UNKNOWN from recent history
|
||||
return recentHistory
|
||||
.lastOrNull { it.caseLidState != DualApplePods.LidState.UNKNOWN }
|
||||
?.caseLidState
|
||||
?: DualApplePods.LidState.NOT_IN_CASE
|
||||
|
||||
@@ -148,7 +148,14 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot,
|
||||
get() = isThisPodInThecase || isOnePodInCase || areBothPodsInCase
|
||||
|
||||
val caseLidState: LidState
|
||||
get() = LidState.fromRaw(pubCaseLidState, hasCaseContext)
|
||||
get() = LidState.fromRaw(
|
||||
raw = pubCaseLidState,
|
||||
hasCaseContext = hasCaseContext,
|
||||
// The lid bit is only trustworthy from a pod broadcasting inside the case (bit 6), or
|
||||
// while both pods are in the case (bit 2). A bit4-only frame comes from the out-of-case
|
||||
// pod and carries a stale lid byte (see LidState.fromRaw).
|
||||
lidReadingReliable = isThisPodInThecase || areBothPodsInCase,
|
||||
)
|
||||
|
||||
/**
|
||||
* TODO this is glitchy
|
||||
@@ -165,8 +172,20 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot,
|
||||
UNKNOWN;
|
||||
|
||||
companion object {
|
||||
fun fromRaw(raw: UByte, hasCaseContext: Boolean): LidState {
|
||||
/**
|
||||
* Derives the lid state from the raw lid byte.
|
||||
*
|
||||
* The open/closed bit is only meaningful when broadcast by a pod that is itself inside
|
||||
* the case ([isThisPodInThecase]) or while both pods are in the case ([areBothPodsInCase]).
|
||||
* When only one pod is in the case and the *other*, out-of-case pod is the one
|
||||
* broadcasting (bit4-only), its lid byte is stale and decodes to a phantom OPEN even while
|
||||
* the case is physically shut (verified on AirPods Pro 1 & Pro 3). Such frames must report
|
||||
* [UNKNOWN] so they don't drive case-open reactions; consumers recover the real state from
|
||||
* an in-case-pod broadcast instead.
|
||||
*/
|
||||
fun fromRaw(raw: UByte, hasCaseContext: Boolean, lidReadingReliable: Boolean): LidState {
|
||||
if (!hasCaseContext) return NOT_IN_CASE
|
||||
if (!lidReadingReliable) return UNKNOWN
|
||||
|
||||
return when ((raw.toInt() shr 3) and 0x01) {
|
||||
0 -> OPEN
|
||||
|
||||
@@ -2,7 +2,9 @@ package eu.darken.capod.pods.core.apple.ble.protocol
|
||||
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -16,6 +18,17 @@ object ProximityPairing {
|
||||
return null
|
||||
}
|
||||
|
||||
// Pods also emit type 0x07 frames whose payload does not use the plaintext status
|
||||
// format, e.g. prefix 0x07 from the identity address on connect (#603). Their bytes
|
||||
// are garbage at the standard offsets and would create a ghost device.
|
||||
if (message.data.firstOrNull() != PAIRING_MESSAGE_PREFIX) {
|
||||
log(TAG, DEBUG) {
|
||||
val hex = message.data.joinToString(" ") { "%02X".format(it.toInt()) }
|
||||
"Dropping proximity pairing message with unsupported prefix: [$hex]"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return ProximityMessage(
|
||||
type = message.type,
|
||||
length = message.length,
|
||||
@@ -49,4 +62,9 @@ object ProximityPairing {
|
||||
|
||||
// This is the default message length among official Apple devices, clones may have different length
|
||||
internal const val PAIRING_MESSAGE_LENGTH = 25
|
||||
|
||||
// First payload byte of every known plaintext status broadcast, including pairing mode
|
||||
internal val PAIRING_MESSAGE_PREFIX = 0x01.toUByte()
|
||||
|
||||
private val TAG = logTag("Pod", "Apple", "ProximityPairing")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ble.protocol.IdentityResolvingKey
|
||||
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityEncryptionKey
|
||||
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
|
||||
import eu.darken.capod.reaction.core.charged.ChargedSlotScope
|
||||
import eu.darken.capod.reaction.core.conversation.ConversationAction
|
||||
import eu.darken.capod.reaction.core.stem.StemActionsConfig
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -25,11 +27,17 @@ data class AppleDeviceProfile(
|
||||
@SerialName("address") override val address: String? = null,
|
||||
@SerialName("reactionAutoPause") val autoPause: Boolean = false,
|
||||
@SerialName("reactionAutoPlay") val autoPlay: Boolean = false,
|
||||
@SerialName("reactionStartMusicOnWear") val startMusicOnWear: Boolean = false,
|
||||
@SerialName("reactionOnePodMode") val onePodMode: Boolean = false,
|
||||
@SerialName("reactionAutoConnect") val autoConnect: Boolean = false,
|
||||
@SerialName("reactionAutoConnectCondition") val autoConnectCondition: AutoConnectCondition = AutoConnectCondition.WHEN_SEEN,
|
||||
@SerialName("reactionShowPopUpOnCaseOpen") val showPopUpOnCaseOpen: Boolean = false,
|
||||
@SerialName("reactionShowPopUpOnConnection") val showPopUpOnConnection: Boolean = false,
|
||||
@SerialName("reactionConversationAction") val conversationAction: ConversationAction = ConversationAction.NOTHING,
|
||||
@SerialName("reactionConversationVolumeReduction") val conversationVolumeReduction: Int = ReactionConfig.DEFAULT_CONVERSATION_VOLUME_REDUCTION,
|
||||
@SerialName("reactionNotifyWhenCharged") val notifyWhenCharged: Boolean = false,
|
||||
@SerialName("reactionChargedThreshold") val chargedThreshold: Int = ReactionConfig.DEFAULT_CHARGED_THRESHOLD,
|
||||
@SerialName("reactionChargedSlotScope") val chargedSlotScope: ChargedSlotScope = ChargedSlotScope.PODS_AND_CASE,
|
||||
/**
|
||||
* Last-known device-side AllowOffOption (AAP setting 0x34). Persisted so the UI can honor
|
||||
* the learned value across sessions — AAP state is dropped on disconnect, but whether OFF
|
||||
@@ -49,11 +57,17 @@ data class AppleDeviceProfile(
|
||||
get() = ReactionConfig(
|
||||
autoPause = autoPause,
|
||||
autoPlay = autoPlay,
|
||||
startMusicOnWear = startMusicOnWear,
|
||||
onePodMode = onePodMode,
|
||||
autoConnect = autoConnect,
|
||||
autoConnectCondition = autoConnectCondition,
|
||||
showPopUpOnCaseOpen = showPopUpOnCaseOpen,
|
||||
showPopUpOnConnection = showPopUpOnConnection,
|
||||
conversationAction = conversationAction,
|
||||
conversationVolumeReduction = conversationVolumeReduction,
|
||||
notifyWhenCharged = notifyWhenCharged,
|
||||
chargedThreshold = chargedThreshold,
|
||||
chargedSlotScope = chargedSlotScope,
|
||||
)
|
||||
|
||||
override fun toString(): String = "AppleDeviceProfile(" +
|
||||
@@ -62,10 +76,16 @@ data class AppleDeviceProfile(
|
||||
"identityKey=${if (identityKey == null) "null" else "<redacted>"}, " +
|
||||
"encryptionKey=${if (encryptionKey == null) "null" else "<redacted>"}, " +
|
||||
"address=$address, autoPause=$autoPause, autoPlay=$autoPlay, " +
|
||||
"startMusicOnWear=$startMusicOnWear, " +
|
||||
"onePodMode=$onePodMode, autoConnect=$autoConnect, " +
|
||||
"autoConnectCondition=$autoConnectCondition, " +
|
||||
"showPopUpOnCaseOpen=$showPopUpOnCaseOpen, " +
|
||||
"showPopUpOnConnection=$showPopUpOnConnection, " +
|
||||
"conversationAction=$conversationAction, " +
|
||||
"conversationVolumeReduction=$conversationVolumeReduction, " +
|
||||
"notifyWhenCharged=$notifyWhenCharged, " +
|
||||
"chargedThreshold=$chargedThreshold, " +
|
||||
"chargedSlotScope=$chargedSlotScope, " +
|
||||
"learnedAllowOffEnabled=$learnedAllowOffEnabled, " +
|
||||
"lastRequestedListeningModeCycleMask=$lastRequestedListeningModeCycleMask, " +
|
||||
"stemActions=$stemActions" +
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
package eu.darken.capod.profiles.core
|
||||
|
||||
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
|
||||
import eu.darken.capod.reaction.core.charged.ChargedSlotScope
|
||||
import eu.darken.capod.reaction.core.conversation.ConversationAction
|
||||
|
||||
data class ReactionConfig(
|
||||
val autoPause: Boolean = false,
|
||||
val autoPlay: Boolean = false,
|
||||
val startMusicOnWear: Boolean = false,
|
||||
val onePodMode: Boolean = false,
|
||||
val autoConnect: Boolean = false,
|
||||
val autoConnectCondition: AutoConnectCondition = AutoConnectCondition.WHEN_SEEN,
|
||||
val showPopUpOnCaseOpen: Boolean = false,
|
||||
val showPopUpOnConnection: Boolean = false,
|
||||
)
|
||||
val conversationAction: ConversationAction = ConversationAction.NOTHING,
|
||||
/** Percentage to lower media volume by when [conversationAction] is LOWER_VOLUME (clamped 10..90 on use). */
|
||||
val conversationVolumeReduction: Int = DEFAULT_CONVERSATION_VOLUME_REDUCTION,
|
||||
val notifyWhenCharged: Boolean = false,
|
||||
/** Battery percentage at which the charged notification fires (clamped 50..100 on use). */
|
||||
val chargedThreshold: Int = DEFAULT_CHARGED_THRESHOLD,
|
||||
val chargedSlotScope: ChargedSlotScope = ChargedSlotScope.PODS_AND_CASE,
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT_CHARGED_THRESHOLD = 100
|
||||
const val MIN_CHARGED_THRESHOLD = 50
|
||||
const val MAX_CHARGED_THRESHOLD = 100
|
||||
const val CHARGED_THRESHOLD_STEP = 10
|
||||
|
||||
const val DEFAULT_CONVERSATION_VOLUME_REDUCTION = 50
|
||||
const val MIN_CONVERSATION_VOLUME_REDUCTION = 10
|
||||
|
||||
/** 100% lowers media volume all the way to 0 — i.e. full mute while speaking. */
|
||||
const val MAX_CONVERSATION_VOLUME_REDUCTION = 100
|
||||
}
|
||||
}
|
||||
|
||||
interface HasReactionConfig {
|
||||
val reactionConfig: ReactionConfig
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.darken.capod.reaction.core.autoconnect
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.bluetooth.NudgeAvailability
|
||||
import eu.darken.capod.common.bluetooth.NudgeCapabilityStore
|
||||
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
|
||||
@@ -26,6 +28,7 @@ import javax.inject.Singleton
|
||||
class AutoConnect @Inject constructor(
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val deviceMonitor: DeviceMonitor,
|
||||
private val nudgeCapabilityStore: NudgeCapabilityStore,
|
||||
) {
|
||||
|
||||
fun monitor(): Flow<Unit> = deviceMonitor.primaryDevice()
|
||||
@@ -36,7 +39,9 @@ class AutoConnect @Inject constructor(
|
||||
combine(
|
||||
bluetoothManager.connectedDevices,
|
||||
deviceMonitor.primaryDevice().filterNotNull().distinctUntilChangedBy {
|
||||
Triple(it.rawDataHex, it.reactions.autoConnectCondition, it.reactions.onePodMode)
|
||||
// Include caseLidState: for the CASE_OPEN condition it is derived from history
|
||||
// and can flip to OPEN while the selected frame's raw bytes are unchanged.
|
||||
listOf(it.rawDataHex, it.reactions.autoConnectCondition, it.reactions.onePodMode, it.caseLidState)
|
||||
},
|
||||
) { connectedDevices, mainDevice ->
|
||||
connectedDevices to mainDevice
|
||||
@@ -98,13 +103,14 @@ class AutoConnect @Inject constructor(
|
||||
return@map
|
||||
}
|
||||
|
||||
if (!bluetoothManager.isNudgeAvailable) {
|
||||
log(TAG, WARN) { "nudgeConnection is not available on this device, skipping" }
|
||||
if (nudgeCapabilityStore.availability.value == NudgeAvailability.BROKEN) {
|
||||
log(TAG, WARN) { "nudgeConnection is known broken on this device, skipping" }
|
||||
return@map
|
||||
}
|
||||
|
||||
val result = bluetoothManager.nudgeConnection(bondedDevice)
|
||||
log(TAG) { "nudgeConnection($bondedDevice) returned $result" }
|
||||
nudgeCapabilityStore.record(result)
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package eu.darken.capod.reaction.core.charged
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.devicesWithProfiles
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.ble.SingleBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.HasCase
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.HasChargeDetection
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.HasChargeDetectionDual
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.ReactionConfig
|
||||
import eu.darken.capod.reaction.core.charged.ChargingSessionStateMachine.Input
|
||||
import eu.darken.capod.reaction.core.charged.ChargingSessionStateMachine.Output
|
||||
import eu.darken.capod.reaction.core.charged.ChargingSessionStateMachine.Slot
|
||||
import eu.darken.capod.reaction.core.charged.ChargingSessionStateMachine.SlotData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ChargedReaction @Inject constructor(
|
||||
private val deviceMonitor: DeviceMonitor,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
) {
|
||||
|
||||
sealed class Event {
|
||||
data class ShowNotification(
|
||||
val profileId: String,
|
||||
val deviceLabel: String,
|
||||
val thresholdPercent: Int,
|
||||
) : Event()
|
||||
|
||||
data class CancelNotification(val profileId: String) : Event()
|
||||
}
|
||||
|
||||
private data class ChargedConfig(val scope: ChargedSlotScope, val thresholdPercent: Int)
|
||||
|
||||
fun monitor(): Flow<Event> = flow {
|
||||
// Machines live in the collection scope: a monitor (service) restart starts fresh sessions.
|
||||
val machines = mutableMapOf<String, ChargingSessionStateMachine>()
|
||||
val configs = mutableMapOf<String, ChargedConfig>()
|
||||
combine(
|
||||
profilesRepo.profiles,
|
||||
deviceMonitor.devicesWithProfiles(),
|
||||
) { profiles, devices -> profiles.filterIsInstance<AppleDeviceProfile>() to devices }
|
||||
.collect { (profiles, devices) ->
|
||||
val enabled = profiles.filter { it.notifyWhenCharged }.associateBy { it.id }
|
||||
|
||||
// Profile deleted or toggle disabled → reset; a missing device snapshot is
|
||||
// handled below as staleness, never as a reset.
|
||||
machines.keys.filter { it !in enabled }.forEach { profileId ->
|
||||
val output = machines.remove(profileId)!!.process(Input.Reset)
|
||||
configs.remove(profileId)
|
||||
if (output == Output.CANCEL) emit(Event.CancelNotification(profileId))
|
||||
}
|
||||
|
||||
for (profile in enabled.values) {
|
||||
val machine = machines.getOrPut(profile.id) { ChargingSessionStateMachine() }
|
||||
val thresholdPercent = profile.chargedThreshold.coerceIn(
|
||||
ReactionConfig.MIN_CHARGED_THRESHOLD,
|
||||
ReactionConfig.MAX_CHARGED_THRESHOLD,
|
||||
)
|
||||
// A persisted CASE scope on a caseless model (restored backup, model change)
|
||||
// would otherwise produce an eternally-empty slot set.
|
||||
val scope = if (profile.model.features.hasCase) profile.chargedSlotScope else ChargedSlotScope.PODS
|
||||
val config = ChargedConfig(scope, thresholdPercent)
|
||||
val previousConfig = configs.put(profile.id, config)
|
||||
if (previousConfig != null && previousConfig != config) {
|
||||
// Scope/threshold changed: stale highwater slots and an already-shown
|
||||
// notification don't fit the new settings — start evaluation over.
|
||||
log(TAG, INFO) { "Settings changed for ${profile.id} ($previousConfig -> $config), resetting" }
|
||||
if (machine.process(Input.Reset) == Output.CANCEL) {
|
||||
emit(Event.CancelNotification(profile.id))
|
||||
}
|
||||
}
|
||||
val device = devices.firstOrNull { it.profileId == profile.id }
|
||||
val slots = device?.liveChargingSlots(scope).orEmpty()
|
||||
// No live slot data (device gone, lid closed, or AAP hasn't pushed battery
|
||||
// yet) is treated as staleness — the session suspends instead of resetting.
|
||||
val input = if (device == null || slots.isEmpty()) {
|
||||
Input.StaleUpdate
|
||||
} else {
|
||||
Input.LiveUpdate(
|
||||
slots = slots,
|
||||
threshold = thresholdPercent / 100f,
|
||||
activity = device.chargingActivity(),
|
||||
)
|
||||
}
|
||||
val phaseBefore = machine.phase
|
||||
val output = machine.process(input)
|
||||
if (machine.phase != phaseBefore) {
|
||||
log(TAG, VERBOSE) {
|
||||
"${profile.id}: $phaseBefore -> ${machine.phase} (output=$output, input=$input)"
|
||||
}
|
||||
}
|
||||
when (output) {
|
||||
Output.SHOW -> {
|
||||
val label = device?.label ?: profile.label
|
||||
log(TAG, INFO) { "Charge complete for ${profile.id} ($label) at $thresholdPercent%" }
|
||||
emit(Event.ShowNotification(profile.id, label, thresholdPercent))
|
||||
}
|
||||
|
||||
Output.CANCEL -> {
|
||||
log(TAG, INFO) { "Charging session over for ${profile.id}, cancelling notification" }
|
||||
emit(Event.CancelNotification(profile.id))
|
||||
}
|
||||
|
||||
Output.NONE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "chargedReaction" }
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Reaction", "Charged")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-slot live readings only — AAP first, then live BLE. The [PodDevice] facade getters are
|
||||
* deliberately bypassed because they silently fall back to the disk cache; stale cached values
|
||||
* must never start, complete, or end a charging session.
|
||||
*/
|
||||
/**
|
||||
* Signals that the user is handling the device, used to dismiss an already-shown notification.
|
||||
* All sources are live (ear detection: AAP/BLE; lid: BLE; DISCONNECTED slots: AAP) — the cache
|
||||
* never feeds in here.
|
||||
*/
|
||||
internal fun PodDevice.chargingActivity(): ChargingSessionStateMachine.Activity =
|
||||
ChargingSessionStateMachine.Activity(
|
||||
// Only trust AAP ear detection — BLE ear bits are phantom for in-case pods (see
|
||||
// PodDevice.hasAapEarDetection). Unknown when there's no AAP session, which is the
|
||||
// normal state while charging in the case, so phantom "worn" never dismisses.
|
||||
wornSlots = when {
|
||||
!hasAapEarDetection -> null
|
||||
hasDualPods -> buildSet {
|
||||
if (isLeftInEar == true) add(Slot.LEFT)
|
||||
if (isRightInEar == true) add(Slot.RIGHT)
|
||||
}
|
||||
else -> if (isBeingWorn == true) setOf(Slot.HEADSET) else emptySet()
|
||||
},
|
||||
lid = when (caseLidState) {
|
||||
DualApplePods.LidState.OPEN -> ChargingSessionStateMachine.Lid.OPEN
|
||||
DualApplePods.LidState.CLOSED -> ChargingSessionStateMachine.Lid.CLOSED
|
||||
DualApplePods.LidState.NOT_IN_CASE -> ChargingSessionStateMachine.Lid.NOT_IN_CASE
|
||||
else -> null
|
||||
},
|
||||
disconnectedSlots = buildSet {
|
||||
if (leftPodChargingState == AapPodState.ChargingState.DISCONNECTED) add(Slot.LEFT)
|
||||
if (rightPodChargingState == AapPodState.ChargingState.DISCONNECTED) add(Slot.RIGHT)
|
||||
if (caseChargingState == AapPodState.ChargingState.DISCONNECTED) add(Slot.CASE)
|
||||
if (headsetChargingState == AapPodState.ChargingState.DISCONNECTED) add(Slot.HEADSET)
|
||||
},
|
||||
)
|
||||
|
||||
internal fun PodDevice.liveChargingSlots(scope: ChargedSlotScope): Map<Slot, SlotData> {
|
||||
val slots = mutableMapOf<Slot, SlotData>()
|
||||
fun add(slot: Slot, battery: Float?, charging: Boolean?) {
|
||||
if (battery == null || battery < 0f || charging == null) return
|
||||
slots[slot] = SlotData(battery = battery, isCharging = charging)
|
||||
}
|
||||
if (scope != ChargedSlotScope.CASE) {
|
||||
add(
|
||||
Slot.LEFT,
|
||||
aap?.batteryLeft ?: (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent,
|
||||
aap?.isLeftCharging ?: (ble as? HasChargeDetectionDual)?.isLeftPodCharging,
|
||||
)
|
||||
add(
|
||||
Slot.RIGHT,
|
||||
aap?.batteryRight ?: (ble as? DualBlePodSnapshot)?.batteryRightPodPercent,
|
||||
aap?.isRightCharging ?: (ble as? HasChargeDetectionDual)?.isRightPodCharging,
|
||||
)
|
||||
add(
|
||||
Slot.HEADSET,
|
||||
aap?.batteryHeadset ?: (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent,
|
||||
aap?.isHeadsetCharging ?: (ble as? HasChargeDetection)?.isHeadsetBeingCharged,
|
||||
)
|
||||
}
|
||||
if (scope != ChargedSlotScope.PODS) {
|
||||
add(
|
||||
Slot.CASE,
|
||||
aap?.batteryCase ?: (ble as? HasCase)?.batteryCasePercent,
|
||||
aap?.isCaseCharging ?: (ble as? HasCase)?.isCaseCharging,
|
||||
)
|
||||
}
|
||||
return slots
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package eu.darken.capod.reaction.core.charged
|
||||
|
||||
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 ChargedReactionNotifications @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManager,
|
||||
) {
|
||||
|
||||
init {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.reaction_charged_channel_label),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun show(profileId: String, deviceLabel: String, thresholdPercent: Int) {
|
||||
if (!notificationManager.areNotificationsEnabled()) {
|
||||
log(TAG, WARN) { "Notifications disabled — charged notification suppressed" }
|
||||
return
|
||||
}
|
||||
val openPi = PendingIntent.getActivity(
|
||||
context,
|
||||
PENDING_INTENT_REQUEST_CODE,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntentCompat.FLAG_IMMUTABLE,
|
||||
)
|
||||
val text = if (thresholdPercent >= 100) {
|
||||
context.getString(R.string.reaction_charged_notification_text_full, deviceLabel)
|
||||
} else {
|
||||
context.getString(R.string.reaction_charged_notification_text_partial, deviceLabel, thresholdPercent)
|
||||
}
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.device_earbuds_generic_both)
|
||||
.setContentTitle(context.getString(R.string.reaction_charged_notification_title))
|
||||
.setContentText(text)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setContentIntent(openPi)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
notificationManager.notify(profileId.toTag(), NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
fun cancel(profileId: String) {
|
||||
notificationManager.cancel(profileId.toTag(), NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops every charged notification still on screen. Used when monitoring stops — without a
|
||||
* running monitor the auto-dismiss-on-unplug promise can't be kept, so don't leave them up.
|
||||
*/
|
||||
fun cancelAll() {
|
||||
notificationManager.activeNotifications
|
||||
.filter { it.id == NOTIFICATION_ID && it.tag?.startsWith(TAG_PREFIX) == true }
|
||||
.forEach { notificationManager.cancel(it.tag, it.id) }
|
||||
}
|
||||
|
||||
private fun String.toTag() = "$TAG_PREFIX$this"
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Reaction", "Charged", "Notifications")
|
||||
private val CHANNEL_ID = "${BuildConfigWrap.APPLICATION_ID}.notification.channel.reaction.charged"
|
||||
private const val NOTIFICATION_ID = 4
|
||||
private const val TAG_PREFIX = "charged:"
|
||||
private const val PENDING_INTENT_REQUEST_CODE = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package eu.darken.capod.reaction.core.charged
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import eu.darken.capod.R
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** Which battery slots the charged notification watches. */
|
||||
@Serializable
|
||||
enum class ChargedSlotScope(
|
||||
@StringRes val labelRes: Int,
|
||||
) {
|
||||
@SerialName("charged.scope.pods") PODS(R.string.settings_charged_scope_pods_label),
|
||||
@SerialName("charged.scope.case") CASE(R.string.settings_charged_scope_case_label),
|
||||
@SerialName("charged.scope.podsandcase") PODS_AND_CASE(R.string.settings_charged_scope_both_label),
|
||||
}
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package eu.darken.capod.reaction.core.charged
|
||||
|
||||
/**
|
||||
* Tracks one charging session for one profile and decides when the "charged" notification
|
||||
* should be shown or cancelled. Pure Kotlin, no Android/DI/Flow dependencies; instances are
|
||||
* driven sequentially from a single coroutine (no internal locking).
|
||||
*
|
||||
* A session starts when any slot reports charging and collects every slot that charges while
|
||||
* the session runs. Per-slot battery highwater marks decide completion: a slot that stops
|
||||
* charging at/above the threshold finished naturally (firmware flips the charging flag off at
|
||||
* 100%), while a slot that stops below the threshold was genuinely unplugged and resets the
|
||||
* session. Stale data suspends the session instead of resetting it — brief BLE gaps while the
|
||||
* device sits on the charger must not lose progress.
|
||||
*
|
||||
* After firing, the notification is taken down again when the user demonstrably reacted to it:
|
||||
* battery discharging below the threshold, a new charging session starting, or any activity
|
||||
* signal showing the pods are being handled (worn-pod set changed, pod removed from the case,
|
||||
* case lid moved). Activity is judged against baselines captured at fire time so pre-existing
|
||||
* state doesn't dismiss — only a change does:
|
||||
* - Worn pods and DISCONNECTED slots baseline on the firing frame. They only apply to
|
||||
* sessions containing pod slots — wearing pods says nothing about a case-only charge.
|
||||
* - The lid signal baselines on the UNION of values seen before the fire (this session plus
|
||||
* a few frames leading into it). With one pod worn and one charging, both pods broadcast
|
||||
* and the monitor's dedup alternates between their frames (OPEN from the in-case pod,
|
||||
* NOT_IN_CASE from the worn pod) — a single-frame baseline would read that steady flapping
|
||||
* as activity. Lid movement is a handling signal for any session, including case-only.
|
||||
*
|
||||
* An activity dismissal latches into [Phase.DISMISSED] so a still-full, still-charging device
|
||||
* can't immediately re-fire.
|
||||
*/
|
||||
class ChargingSessionStateMachine {
|
||||
|
||||
enum class Slot { LEFT, RIGHT, CASE, HEADSET }
|
||||
|
||||
/** [battery] is a fraction 0..1, matching the monitor layer's battery values. */
|
||||
data class SlotData(val battery: Float, val isCharging: Boolean)
|
||||
|
||||
/**
|
||||
* Case posture from the BLE lid byte. NOT_IN_CASE isn't a lid position — it means the
|
||||
* broadcasting pod is physically outside the case, which is just as much a handling
|
||||
* signal as a lid movement (it's the only BLE-side trace of "pod taken out").
|
||||
*/
|
||||
enum class Lid { OPEN, CLOSED, NOT_IN_CASE }
|
||||
|
||||
/** Signals that the user is physically handling the device. All optional/unknown-safe. */
|
||||
data class Activity(
|
||||
/** Pods currently worn (per-side); null when no ear data at all. */
|
||||
val wornSlots: Set<Slot>? = null,
|
||||
val lid: Lid? = null,
|
||||
/** Slots physically absent from the case (AAP ChargingState.DISCONNECTED). */
|
||||
val disconnectedSlots: Set<Slot> = emptySet(),
|
||||
)
|
||||
|
||||
sealed interface Input {
|
||||
/**
|
||||
* Fresh data from a live source (BLE/AAP). [slots] only contains slots with live
|
||||
* readings — cached values must never be fed in here. [threshold] is a fraction 0..1.
|
||||
*/
|
||||
data class LiveUpdate(
|
||||
val slots: Map<Slot, SlotData>,
|
||||
val threshold: Float,
|
||||
val activity: Activity = Activity(),
|
||||
) : Input
|
||||
|
||||
/** Device has no live data right now (out of range, lid closed, cache-only). */
|
||||
data object StaleUpdate : Input
|
||||
|
||||
/** Toggle disabled, settings changed, or profile deleted. */
|
||||
data object Reset : Input
|
||||
}
|
||||
|
||||
enum class Output { NONE, SHOW, CANCEL }
|
||||
|
||||
enum class Phase { IDLE, CHARGING, FIRED, DISMISSED, SUSPENDED }
|
||||
|
||||
var phase: Phase = Phase.IDLE
|
||||
private set
|
||||
|
||||
/** Phase to resume into when live data returns; only meaningful while SUSPENDED. */
|
||||
private var suspendedFrom: Phase = Phase.IDLE
|
||||
|
||||
/** Battery highwater per slot that joined the session. */
|
||||
private val highwater = mutableMapOf<Slot, Float>()
|
||||
|
||||
/**
|
||||
* Rolling window of the most recent lid observations across all phases. Seeds the at-fire
|
||||
* baseline so the OPEN/NOT_IN_CASE alternation (worn pod broadcasts NOT_IN_CASE while the
|
||||
* in-case pod broadcasts OPEN; the monitor's dedup serves them in turn) is absorbed — even
|
||||
* for an instant fire right after a settings-change reset, since the window is never cleared.
|
||||
*/
|
||||
private val recentLids = ArrayDeque<Lid>()
|
||||
|
||||
// Activity baselines frozen at fire; see class docs.
|
||||
private val baselineLids = mutableSetOf<Lid>()
|
||||
private var baselineWorn: Set<Slot>? = null
|
||||
private var baselineDisconnected: Set<Slot> = emptySet()
|
||||
|
||||
fun process(input: Input): Output {
|
||||
if (input is Input.LiveUpdate) input.activity.lid?.let(::recordRecentLid)
|
||||
return when (phase) {
|
||||
Phase.IDLE -> processIdle(input)
|
||||
Phase.CHARGING -> processCharging(input)
|
||||
Phase.FIRED -> processFired(input)
|
||||
Phase.DISMISSED -> processDismissed(input)
|
||||
Phase.SUSPENDED -> processSuspended(input)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processIdle(input: Input): Output {
|
||||
if (input !is Input.LiveUpdate) return Output.NONE
|
||||
val charging = input.slots.filterValues { it.isCharging }
|
||||
if (charging.isEmpty()) return Output.NONE
|
||||
highwater.clear()
|
||||
charging.forEach { (slot, data) -> highwater[slot] = data.battery }
|
||||
phase = Phase.CHARGING
|
||||
return fireIfComplete(input)
|
||||
}
|
||||
|
||||
private fun processCharging(input: Input): Output = when (input) {
|
||||
is Input.Reset -> reset(cancel = false)
|
||||
is Input.StaleUpdate -> suspend(Phase.CHARGING)
|
||||
is Input.LiveUpdate -> {
|
||||
// Charging slots join the session (e.g. case plugged in later); slots already in
|
||||
// it advance their mark; slots absent from this update stay frozen.
|
||||
input.slots.forEach { (slot, data) ->
|
||||
if (data.isCharging || slot in highwater) {
|
||||
highwater[slot] = maxOf(highwater[slot] ?: 0f, data.battery)
|
||||
}
|
||||
}
|
||||
val unplugged = highwater.any { (slot, mark) ->
|
||||
input.slots[slot]?.isCharging == false && mark < input.threshold
|
||||
}
|
||||
if (unplugged) reset(cancel = false) else fireIfComplete(input)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processFired(input: Input): Output = when (input) {
|
||||
is Input.Reset -> reset(cancel = true)
|
||||
is Input.StaleUpdate -> suspend(Phase.FIRED)
|
||||
is Input.LiveUpdate -> when {
|
||||
// "All slots stopped charging" alone is NOT an unplug signal — firmware flips the
|
||||
// charging flag off at 100% while still on power. Slots that never charged this
|
||||
// session (e.g. an idle half-empty case) carry no signal either. The session ends
|
||||
// on: a new charge context, discharge below threshold, or user activity.
|
||||
isNewChargeContext(input) -> {
|
||||
// Only CANCEL is reported now; the next update finds IDLE and starts (and, if
|
||||
// the data already satisfies the threshold, fires) the new session.
|
||||
phase = Phase.IDLE
|
||||
highwater.clear()
|
||||
Output.CANCEL
|
||||
}
|
||||
|
||||
isSessionDischarged(input) -> reset(cancel = true)
|
||||
|
||||
isActivityDetected(input.activity) -> {
|
||||
phase = Phase.DISMISSED
|
||||
Output.CANCEL
|
||||
}
|
||||
|
||||
else -> Output.NONE
|
||||
}
|
||||
}
|
||||
|
||||
/** Like FIRED, but the notification is already gone — same exits, silent. */
|
||||
private fun processDismissed(input: Input): Output = when (input) {
|
||||
is Input.Reset -> reset(cancel = false)
|
||||
is Input.StaleUpdate -> suspend(Phase.DISMISSED)
|
||||
is Input.LiveUpdate -> when {
|
||||
isNewChargeContext(input) -> {
|
||||
// Nothing to cancel, so unlike FIRED the new session can start right away.
|
||||
phase = Phase.IDLE
|
||||
highwater.clear()
|
||||
processIdle(input)
|
||||
}
|
||||
|
||||
isSessionDischarged(input) -> reset(cancel = false)
|
||||
|
||||
else -> Output.NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun processSuspended(input: Input): Output = when (input) {
|
||||
is Input.Reset -> reset(cancel = suspendedFrom == Phase.FIRED)
|
||||
is Input.StaleUpdate -> Output.NONE
|
||||
is Input.LiveUpdate -> {
|
||||
phase = suspendedFrom
|
||||
if (phase == Phase.CHARGING && hasRegressed(input)) {
|
||||
// Battery dropped well below a session mark while we were blind — the device
|
||||
// was unplugged and used in the meantime. Start over with the fresh data.
|
||||
phase = Phase.IDLE
|
||||
highwater.clear()
|
||||
processIdle(input)
|
||||
} else {
|
||||
process(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Something is charging below the threshold — a fresh charging session is beginning. */
|
||||
private fun isNewChargeContext(input: Input.LiveUpdate): Boolean =
|
||||
input.slots.any { it.value.isCharging && it.value.battery < input.threshold }
|
||||
|
||||
/** A session slot is off power and drained below the threshold — device is in use. */
|
||||
private fun isSessionDischarged(input: Input.LiveUpdate): Boolean = highwater.keys.any { slot ->
|
||||
input.slots[slot]?.let { !it.isCharging && it.battery < input.threshold } == true
|
||||
}
|
||||
|
||||
private fun isActivityDetected(activity: Activity): Boolean {
|
||||
// Worn pods and pod removal only speak about pod slots; a case-only session ignores them.
|
||||
if (highwater.keys.any { it != Slot.CASE }) {
|
||||
activity.wornSlots?.let { worn ->
|
||||
when (val baseline = baselineWorn) {
|
||||
// No ear data at fire time: pods can't be worn while charging in the case,
|
||||
// so any worn pod is activity — only "nothing worn" is adoptable as baseline.
|
||||
null -> if (worn.isNotEmpty()) return true else baselineWorn = emptySet()
|
||||
else -> if (worn != baseline) return true
|
||||
}
|
||||
}
|
||||
val newlyDisconnected = activity.disconnectedSlots.any {
|
||||
it != Slot.CASE && it in highwater && it !in baselineDisconnected
|
||||
}
|
||||
if (newlyDisconnected) return true
|
||||
}
|
||||
activity.lid?.let { lid ->
|
||||
if (baselineLids.isEmpty()) {
|
||||
// Lid never observed pre-fire: first sighting is the baseline — it was most
|
||||
// likely in that posture all along (open is the normal charging posture).
|
||||
baselineLids.add(lid)
|
||||
} else if (lid !in baselineLids) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasRegressed(input: Input.LiveUpdate): Boolean = highwater.any { (slot, mark) ->
|
||||
input.slots[slot]?.let { it.battery < mark - REGRESSION_TOLERANCE } == true
|
||||
}
|
||||
|
||||
private fun recordRecentLid(lid: Lid) {
|
||||
recentLids.addLast(lid)
|
||||
while (recentLids.size > RECENT_LID_FRAMES) recentLids.removeFirst()
|
||||
}
|
||||
|
||||
private fun fireIfComplete(input: Input.LiveUpdate): Output {
|
||||
if (highwater.isEmpty() || highwater.any { it.value < input.threshold }) return Output.NONE
|
||||
phase = Phase.FIRED
|
||||
baselineLids.clear()
|
||||
baselineLids += recentLids
|
||||
baselineWorn = input.activity.wornSlots
|
||||
baselineDisconnected = input.activity.disconnectedSlots.intersect(highwater.keys)
|
||||
return Output.SHOW
|
||||
}
|
||||
|
||||
private fun reset(cancel: Boolean): Output {
|
||||
phase = Phase.IDLE
|
||||
highwater.clear()
|
||||
// recentLids is an ambient rolling window — intentionally kept across resets so an
|
||||
// instant re-fire after a settings change still has the recent lid alternation.
|
||||
baselineLids.clear()
|
||||
baselineWorn = null
|
||||
baselineDisconnected = emptySet()
|
||||
return if (cancel) Output.CANCEL else Output.NONE
|
||||
}
|
||||
|
||||
private fun suspend(from: Phase): Output {
|
||||
suspendedFrom = from
|
||||
phase = Phase.SUSPENDED
|
||||
return Output.NONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Battery drop (fraction) below a slot's highwater mark that counts as a real
|
||||
* regression on resume. Public BLE reports in 10% steps, so a single-step flicker
|
||||
* (exactly 0.1) must not reset the session.
|
||||
*/
|
||||
private const val REGRESSION_TOLERANCE = 0.1f
|
||||
|
||||
/**
|
||||
* Pre-fire lid observations kept as baseline context. At the observed 0.5-1.5s
|
||||
* emission cadence this is a few seconds — enough to capture dedup alternation
|
||||
* leading into an instant fire without dragging in long-stale postures.
|
||||
*/
|
||||
private const val RECENT_LID_FRAMES = 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.darken.capod.reaction.core.conversation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import eu.darken.capod.R
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* What CAPod does when the pods report that the wearer started speaking (Conversational
|
||||
* Awareness). On Android the firmware does not duck audio on its own, so CAPod performs the
|
||||
* reaction itself. The action is reverted when speaking stops.
|
||||
*
|
||||
* Extend by adding entries — the persisted [SerialName] strings are stable identifiers.
|
||||
*/
|
||||
@Serializable
|
||||
enum class ConversationAction(
|
||||
val identifier: String,
|
||||
@StringRes val labelRes: Int,
|
||||
) {
|
||||
@SerialName("conversation.action.nothing")
|
||||
NOTHING("conversation.action.nothing", R.string.settings_conversation_action_nothing_label),
|
||||
|
||||
@SerialName("conversation.action.lower_volume")
|
||||
LOWER_VOLUME("conversation.action.lower_volume", R.string.settings_conversation_action_lower_volume_label),
|
||||
|
||||
@SerialName("conversation.action.pause")
|
||||
PAUSE("conversation.action.pause", R.string.settings_conversation_action_pause_label),
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package eu.darken.capod.reaction.core.conversation
|
||||
|
||||
import eu.darken.capod.common.MediaControl
|
||||
import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
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.PodDevice
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.ConversationAwarenessEvent
|
||||
import eu.darken.capod.profiles.core.ReactionConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Reacts to Conversational Awareness speaking transitions (AAP `0x4B`) by either lowering media
|
||||
* volume or pausing, per the primary device's [ReactionConfig.conversationAction], and reverts when
|
||||
* speaking stops. On Android the pod firmware does not duck audio itself, so CAPod performs it.
|
||||
*
|
||||
* Disengage is driven by the pod's explicit end-of-speech frame ([ConversationAwarenessEvent.STOP]).
|
||||
* The pod sends NO frames during active speech — it stays engaged for as long as it hears nearby
|
||||
* voices (observed: CA held engaged 21-32s with zero `0x4B` frames, indefinitely against ambient
|
||||
* noise). So frame-silence must NOT be read as "speaking ended"; after a START only the long
|
||||
* [STALE_TIMEOUT] backstop applies, and a link drop is handled by the owner-disconnect revert.
|
||||
*
|
||||
* Because the pod is silent while engaged, any non-START frame is evidence the wind-down has begun.
|
||||
* The wind-down is a short flurry of transitional ([ConversationAwarenessEvent.HOLD]) and terminal
|
||||
* frames — but the terminal is sometimes dropped entirely (fw `…6589` emitted `3,0xB,4` then nothing,
|
||||
* stranding the volume low — #608). So a HOLD frame re-arms the timer with the short
|
||||
* [WIND_DOWN_TIMEOUT] fuse: if no terminal follows, speech is treated as ended anyway.
|
||||
*
|
||||
* State is a single global slot (media volume / playback is system-wide, not per-device) guarded by
|
||||
* a [Mutex] — events, AAP-state-removal, the stale timer, and monitor completion all mutate it.
|
||||
* Volume ducks are additionally reverted on owner-disconnect and monitor completion so a dropped
|
||||
* session never strands the user at low volume.
|
||||
*/
|
||||
@Singleton
|
||||
class ConversationReaction @Inject constructor(
|
||||
private val aapManager: AapConnectionManager,
|
||||
private val deviceMonitor: DeviceMonitor,
|
||||
private val mediaControl: MediaControl,
|
||||
@AppScope private val appScope: CoroutineScope,
|
||||
private val timeSource: TimeSource,
|
||||
) {
|
||||
|
||||
private sealed interface Kind {
|
||||
data object Paused : Kind
|
||||
data class Ducked(val priorVolume: Int, val appliedVolume: Int) : Kind
|
||||
}
|
||||
|
||||
private data class Active(
|
||||
val id: Long,
|
||||
val owner: BluetoothAddress,
|
||||
val kind: Kind,
|
||||
val at: Long,
|
||||
)
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var active: Active? = null
|
||||
private var staleJob: Job? = null
|
||||
private var idCounter = 0L
|
||||
|
||||
fun monitor(): Flow<Unit> = merge(
|
||||
aapManager.conversationalAwarenessEvents.onEach { (address, event) -> onEvent(address, event) },
|
||||
// Reverts a stranded duck when the owning device leaves the AAP state map for any reason
|
||||
// (intentional or not) — disconnectEvents only fires for unintentional drops.
|
||||
aapManager.allStates.onEach { states -> onActiveDevicesChanged(states.keys) },
|
||||
)
|
||||
.map { }
|
||||
// Service stop / scope cancellation: undo any active duck so we don't leave volume lowered.
|
||||
.onCompletion { withContext(NonCancellable) { onMonitorCompleted() } }
|
||||
.setupCommonEventHandlers(TAG) { "conversationReaction" }
|
||||
|
||||
private suspend fun onEvent(address: BluetoothAddress, event: ConversationAwarenessEvent) = when (event) {
|
||||
ConversationAwarenessEvent.START -> onSpeakingStart(address)
|
||||
ConversationAwarenessEvent.HOLD -> onSpeakingHold(address)
|
||||
ConversationAwarenessEvent.STOP -> onSpeakingStop(address)
|
||||
}
|
||||
|
||||
private suspend fun onSpeakingStart(address: BluetoothAddress) {
|
||||
val primary = deviceMonitor.primaryDevice().first()
|
||||
if (primary?.address != address) {
|
||||
log(TAG) { "START from $address ignored — not primary device (primary=${primary?.address})" }
|
||||
return
|
||||
}
|
||||
val action = primary.reactions.conversationAction
|
||||
if (action == ConversationAction.NOTHING) return
|
||||
|
||||
mutex.withLock {
|
||||
val current = active
|
||||
if (current != null && current.owner == address) {
|
||||
// Duplicate START for the same speaker — don't re-act, just keep the session alive.
|
||||
// A START also cancels a pending wind-down fuse: the wearer is speaking again.
|
||||
armDisengageTimer(current, STALE_TIMEOUT)
|
||||
log(TAG) { "START from $address — already active ($action), keep-alive" }
|
||||
return
|
||||
}
|
||||
// A different device started speaking while we were active — undo the old one first.
|
||||
if (current != null) revert(current, "superseded by $address")
|
||||
|
||||
when (action) {
|
||||
ConversationAction.PAUSE -> {
|
||||
val paused = mediaControl.sendPause(rememberForResume = false)
|
||||
if (paused) {
|
||||
val record = Active(nextId(), address, Kind.Paused, timeSource.elapsedRealtime())
|
||||
active = record
|
||||
armDisengageTimer(record, STALE_TIMEOUT)
|
||||
log(TAG, INFO) { "START on $address → paused media" }
|
||||
} else {
|
||||
active = null
|
||||
log(TAG) { "START on $address → nothing playing, no pause" }
|
||||
}
|
||||
}
|
||||
|
||||
ConversationAction.LOWER_VOLUME -> {
|
||||
val reduction = primary.reactions.conversationVolumeReduction
|
||||
.coerceIn(
|
||||
ReactionConfig.MIN_CONVERSATION_VOLUME_REDUCTION,
|
||||
ReactionConfig.MAX_CONVERSATION_VOLUME_REDUCTION,
|
||||
)
|
||||
val duck = mediaControl.duckMusicVolume(reduction)
|
||||
if (duck != null) {
|
||||
val record = Active(
|
||||
nextId(),
|
||||
address,
|
||||
Kind.Ducked(duck.priorVolume, duck.appliedVolume),
|
||||
timeSource.elapsedRealtime(),
|
||||
)
|
||||
active = record
|
||||
armDisengageTimer(record, STALE_TIMEOUT)
|
||||
log(TAG, INFO) { "START on $address → ducked volume ${duck.priorVolume}→${duck.appliedVolume}" }
|
||||
} else {
|
||||
active = null
|
||||
log(TAG) { "START on $address → duck no-op" }
|
||||
}
|
||||
}
|
||||
|
||||
ConversationAction.NOTHING -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitional wind-down frame. The pod is silent during active speech, so this frame means the
|
||||
* wind-down has begun and a terminal frame is imminent — but firmware sometimes drops it (#608).
|
||||
* Arm the short fuse: if no terminal (or fresh START) follows, disengage anyway.
|
||||
*/
|
||||
private suspend fun onSpeakingHold(address: BluetoothAddress) = mutex.withLock {
|
||||
val current = active ?: return
|
||||
if (current.owner != address) return
|
||||
armDisengageTimer(current, WIND_DOWN_TIMEOUT)
|
||||
}
|
||||
|
||||
private suspend fun onSpeakingStop(address: BluetoothAddress) {
|
||||
val primary = deviceMonitor.primaryDevice().first()
|
||||
mutex.withLock {
|
||||
val current = active ?: return
|
||||
if (current.owner != address) {
|
||||
log(TAG) { "STOP from $address ignored — owner is ${current.owner}" }
|
||||
return
|
||||
}
|
||||
clearActive()
|
||||
disengage(current, primary, "STOP on $address")
|
||||
}
|
||||
}
|
||||
|
||||
/** Graceful disengage (STOP event or stale timeout). Must be called under [mutex]. */
|
||||
private suspend fun disengage(record: Active, primary: PodDevice?, reason: String) {
|
||||
when (val kind = record.kind) {
|
||||
is Kind.Paused -> {
|
||||
// Resume the pause WE caused, regardless of the current action setting. Gating on
|
||||
// "action still == PAUSE" would strand media paused if the user switched the action
|
||||
// (or set NOTHING) mid-conversation — undoing our own side effect is the least
|
||||
// surprising behaviour. The remaining guards are about real device/playback state.
|
||||
val age = timeSource.elapsedRealtime() - record.at
|
||||
when {
|
||||
age.milliseconds > PAUSE_RESUME_WINDOW ->
|
||||
log(TAG) { "$reason — resume skipped (stale, ${age}ms)" }
|
||||
primary?.address != record.owner ->
|
||||
log(TAG) { "$reason — resume skipped (primary switched)" }
|
||||
primary.isBeingWorn == false ->
|
||||
log(TAG) { "$reason — resume skipped (not worn)" }
|
||||
mediaControl.isPlaying ->
|
||||
log(TAG) { "$reason — resume skipped (already playing)" }
|
||||
else -> {
|
||||
mediaControl.sendPlay()
|
||||
log(TAG, INFO) { "$reason → resumed media" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Kind.Ducked -> revertDuck(kind, reason)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onActiveDevicesChanged(addresses: Set<BluetoothAddress>) = mutex.withLock {
|
||||
val current = active ?: return
|
||||
if (current.owner !in addresses) {
|
||||
clearActive()
|
||||
revert(current, "owner ${current.owner} gone")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onMonitorCompleted() = mutex.withLock {
|
||||
val current = active ?: return
|
||||
clearActive()
|
||||
revert(current, "monitor completed")
|
||||
}
|
||||
|
||||
/** Forced revert (disconnect / supersede / shutdown): undo volume ducks; leave pauses as-is. */
|
||||
private fun revert(record: Active, reason: String) {
|
||||
when (val kind = record.kind) {
|
||||
is Kind.Ducked -> revertDuck(kind, reason)
|
||||
is Kind.Paused -> log(TAG) { "Clearing pause ($reason) — leaving playback as-is" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun revertDuck(kind: Kind.Ducked, reason: String) {
|
||||
// Restore to the pre-duck volume unconditionally — we do NOT gate on
|
||||
// currentMusicVolume() == appliedVolume. Such a guard skips the restore whenever anything
|
||||
// else moved the volume meanwhile (e.g. a concurrent volume-manager app), which lets the
|
||||
// baseline ratchet down across conversations. Restoring deterministically to the level we
|
||||
// saved at engage avoids that; the trade-off is overriding a deliberate mid-conversation
|
||||
// volume change (rare). Matches librepods' unconditional restore.
|
||||
log(TAG, INFO) { "Restoring volume to ${kind.priorVolume} (ducked to ${kind.appliedVolume}, now ${mediaControl.currentMusicVolume()}, $reason)" }
|
||||
mediaControl.restoreMusicVolume(kind.priorVolume)
|
||||
}
|
||||
|
||||
/** Must be called under [mutex]. Clears the active slot and cancels its stale timer. */
|
||||
private fun clearActive() {
|
||||
staleJob?.cancel()
|
||||
staleJob = null
|
||||
active = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called under [mutex]. (Re)arms the disengage timer for [record] — every frame picks
|
||||
* the fuse matching its meaning: START → [STALE_TIMEOUT] (active speech, frames cease for its
|
||||
* whole duration), HOLD → [WIND_DOWN_TIMEOUT] (wind-down begun, terminal imminent). On expiry
|
||||
* the session is force-ended. Identity-checked so a late timer can't disengage a newer session.
|
||||
*/
|
||||
private fun armDisengageTimer(record: Active, timeout: Duration) {
|
||||
staleJob?.cancel()
|
||||
staleJob = appScope.launch {
|
||||
delay(timeout)
|
||||
val primary = deviceMonitor.primaryDevice().first()
|
||||
mutex.withLock {
|
||||
if (active?.id == record.id) {
|
||||
val current = active!!
|
||||
clearActive()
|
||||
disengage(current, primary, "no terminal frame within $timeout")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextId(): Long = ++idCounter
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Reaction", "Conversation")
|
||||
|
||||
/**
|
||||
* Backstop fuse while engaged with no wind-down evidence yet (only START frames seen).
|
||||
* Must stay LONG: the pod sends zero frames during active speech and stays engaged against
|
||||
* ambient noise, so a short timeout here resumes media mid-conversation (the original 12s
|
||||
* value did exactly that). Only recovers a session whose entire wind-down flurry was lost
|
||||
* while the link stays up. Kept longer than [PAUSE_RESUME_WINDOW] so a back-stopped pause
|
||||
* is never auto-resumed (only a duck is restored — a stranded low volume is the worse
|
||||
* failure).
|
||||
*/
|
||||
private val STALE_TIMEOUT = 5.minutes
|
||||
|
||||
/**
|
||||
* Short fuse armed by a transitional [ConversationAwarenessEvent.HOLD] frame: the wind-down
|
||||
* has begun, so a terminal frame should follow within seconds. With only one pod worn the
|
||||
* pod deterministically drops the terminal (#608, reproduced on Pro 3 and Pro 2) — this
|
||||
* fuse disengages instead of stranding the volume low for [STALE_TIMEOUT]. Must be ≥ ~5s:
|
||||
* gaps up to 2.8s were observed between consecutive wind-down frames, and each HOLD re-arms
|
||||
* this fuse. A fresh START re-arms the long fuse (speaking resumed).
|
||||
*/
|
||||
private val WIND_DOWN_TIMEOUT = 6.seconds
|
||||
|
||||
/** A pause older than this no longer auto-resumes — unexpected late playback is worse. */
|
||||
private val PAUSE_RESUME_WINDOW = 2.minutes
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,7 @@ class PlayPause @Inject constructor(
|
||||
onePodMode = reactions.onePodMode,
|
||||
isCurrentlyPlaying = isCurrentlyPlaying,
|
||||
wasRecentlyPausedByUs = wasRecentlyPausedByUs,
|
||||
startMusicOnWear = reactions.startMusicOnWear,
|
||||
)
|
||||
|
||||
// BLE-only autoplay confirmation only applies to UNAUTHENTICATED sources.
|
||||
@@ -204,6 +205,7 @@ class PlayPause @Inject constructor(
|
||||
shouldStageBleOnlyPlay = shouldStageBleOnlyPlay,
|
||||
isCurrentlyPlaying = isCurrentlyPlaying,
|
||||
wasRecentlyPausedByUs = wasRecentlyPausedByUs,
|
||||
startMusicOnWear = reactions.startMusicOnWear,
|
||||
)
|
||||
pendingPlayConfirmation = confirmation.pending
|
||||
|
||||
@@ -263,7 +265,7 @@ class PlayPause @Inject constructor(
|
||||
}
|
||||
|
||||
decision.shouldPause && reactions.autoPause -> {
|
||||
val pauseSent = mediaControl.sendPause()
|
||||
val pauseSent = mediaControl.sendPause(rememberForResume = true)
|
||||
log(TAG, INFO) {
|
||||
"autoPause triggered: source=$source, " +
|
||||
"wasWorn=${prevState.bothInEar}, isWorn=${currState.bothInEar}, " +
|
||||
@@ -288,10 +290,11 @@ class PlayPause @Inject constructor(
|
||||
onePodMode: Boolean,
|
||||
isCurrentlyPlaying: Boolean,
|
||||
wasRecentlyPausedByUs: Boolean = false,
|
||||
startMusicOnWear: Boolean = false,
|
||||
): PlayPauseDecision = if (onePodMode) {
|
||||
evaluateOnePodMode(previous, current, isCurrentlyPlaying, wasRecentlyPausedByUs)
|
||||
evaluateOnePodMode(previous, current, isCurrentlyPlaying, wasRecentlyPausedByUs, startMusicOnWear)
|
||||
} else {
|
||||
evaluateNormalMode(previous, current, isCurrentlyPlaying, wasRecentlyPausedByUs)
|
||||
evaluateNormalMode(previous, current, isCurrentlyPlaying, wasRecentlyPausedByUs, startMusicOnWear)
|
||||
}
|
||||
|
||||
private fun evaluateOnePodMode(
|
||||
@@ -299,6 +302,7 @@ class PlayPause @Inject constructor(
|
||||
current: EarDetectionState,
|
||||
isCurrentlyPlaying: Boolean,
|
||||
wasRecentlyPausedByUs: Boolean,
|
||||
startMusicOnWear: Boolean,
|
||||
): PlayPauseDecision {
|
||||
val netChange = current.podCount - previous.podCount
|
||||
|
||||
@@ -310,8 +314,13 @@ class PlayPause @Inject constructor(
|
||||
reason = "One-pod mode: pod(s) removed (net change: $netChange)"
|
||||
)
|
||||
|
||||
// Net increase: pod(s) inserted → play
|
||||
netChange > 0 && (!isCurrentlyPlaying || wasRecentlyPausedByUs) -> PlayPauseDecision(
|
||||
// Net increase: pod(s) inserted → play, but only when we paused recently OR the user
|
||||
// opted in to firing a play key on cold wear. Without an opt-in, suppress autoplay
|
||||
// so we never override a user-initiated pause or fire over silence.
|
||||
netChange > 0 && (
|
||||
wasRecentlyPausedByUs ||
|
||||
(startMusicOnWear && !isCurrentlyPlaying)
|
||||
) -> PlayPauseDecision(
|
||||
shouldPlay = true,
|
||||
shouldPause = false,
|
||||
reason = "One-pod mode: pod(s) inserted (net change: +$netChange)",
|
||||
@@ -332,13 +341,19 @@ class PlayPause @Inject constructor(
|
||||
current: EarDetectionState,
|
||||
isCurrentlyPlaying: Boolean,
|
||||
wasRecentlyPausedByCap: Boolean,
|
||||
startMusicOnWear: Boolean,
|
||||
): PlayPauseDecision {
|
||||
val wasWorn = previous.bothInEar
|
||||
val isWorn = current.bothInEar
|
||||
|
||||
return when {
|
||||
// Transition: not worn → worn, and not playing → play
|
||||
!wasWorn && isWorn && (!isCurrentlyPlaying || wasRecentlyPausedByCap) -> PlayPauseDecision(
|
||||
// Transition: not worn → worn → play, but only when we paused recently OR the user
|
||||
// opted in to firing a play key on cold wear. Without an opt-in, suppress autoplay
|
||||
// so we never override a user-initiated pause or fire over silence.
|
||||
!wasWorn && isWorn && (
|
||||
wasRecentlyPausedByCap ||
|
||||
(startMusicOnWear && !isCurrentlyPlaying)
|
||||
) -> PlayPauseDecision(
|
||||
shouldPlay = true,
|
||||
shouldPause = false,
|
||||
reason = "Normal mode: both pods in ear",
|
||||
@@ -371,6 +386,7 @@ class PlayPause @Inject constructor(
|
||||
shouldStageBleOnlyPlay: Boolean,
|
||||
isCurrentlyPlaying: Boolean,
|
||||
wasRecentlyPausedByUs: Boolean,
|
||||
startMusicOnWear: Boolean = false,
|
||||
): PlayConfirmationResult {
|
||||
val activePending = pending?.takeIf {
|
||||
it.profileId == profileId && it.onePodMode == onePodMode && autoPlayEnabled
|
||||
@@ -378,7 +394,10 @@ class PlayPause @Inject constructor(
|
||||
|
||||
if (activePending != null &&
|
||||
currentState == activePending.targetState &&
|
||||
(!isCurrentlyPlaying || wasRecentlyPausedByUs)
|
||||
(
|
||||
wasRecentlyPausedByUs ||
|
||||
(startMusicOnWear && !isCurrentlyPlaying)
|
||||
)
|
||||
) {
|
||||
return PlayConfirmationResult(
|
||||
decision = PlayPauseDecision(
|
||||
|
||||
@@ -13,9 +13,12 @@ import eu.darken.capod.monitor.core.DeviceMonitor
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import java.time.Duration
|
||||
@@ -34,8 +37,12 @@ class PopUpReaction @Inject constructor(
|
||||
|
||||
private fun monitorCase(): Flow<Event> = deviceMonitor.primaryDevice()
|
||||
.distinctUntilChangedBy {
|
||||
// Re-emit on profile changes (eligibility) AND on raw BLE state changes (lid).
|
||||
Triple(it?.profileId, it?.reactions?.showPopUpOnCaseOpen, it?.rawDataHex)
|
||||
// Re-emit on profile changes (eligibility), raw BLE changes (content), AND the derived
|
||||
// lid state. The latter is essential: caseLidState is recovered from history, so it can
|
||||
// flip OPEN<->CLOSED while the *selected* frame's raw bytes stay identical (e.g. a steady
|
||||
// out-of-case frame while a sibling in-case frame updates). Keying on rawDataHex alone
|
||||
// would swallow that transition and the popup would miss its show/hide.
|
||||
listOf(it?.profileId, it?.reactions?.showPopUpOnCaseOpen, it?.rawDataHex, it?.caseLidState)
|
||||
}
|
||||
.withPrevious()
|
||||
.setupCommonEventHandlers(TAG) { "popUpCase" }
|
||||
@@ -71,7 +78,7 @@ class PopUpReaction @Inject constructor(
|
||||
throttleCasePopUps(current)
|
||||
}
|
||||
|
||||
private fun throttleCasePopUps(current: PodDevice): Event? {
|
||||
internal fun throttleCasePopUps(current: PodDevice): Event? {
|
||||
val cooldownKey = current.profileId ?: current.identifier?.toString() ?: return null
|
||||
val now = timeSource.now()
|
||||
val lastShown = caseCoolDowns[cooldownKey]
|
||||
@@ -95,9 +102,9 @@ class PopUpReaction @Inject constructor(
|
||||
}
|
||||
|
||||
decision.shouldHide -> {
|
||||
if (!decision.shouldResetCooldown) {
|
||||
caseCoolDowns[cooldownKey] = now
|
||||
}
|
||||
// Don't stamp the cooldown on a non-CLOSED hide (UNKNOWN/NOT_IN_CASE). Refreshing it
|
||||
// here would let a transient UNKNOWN (e.g. a brief out-of-case frame) suppress a
|
||||
// genuine OPEN for the whole cooldown window. CLOSED still resets it above.
|
||||
Event.PopupHide(now)
|
||||
}
|
||||
|
||||
@@ -192,7 +199,48 @@ class PopUpReaction @Inject constructor(
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "popUpConnection" }
|
||||
|
||||
fun monitor(): Flow<Event> = merge(monitorCase(), monitorConnection())
|
||||
/**
|
||||
* Backstop for case-open popups that never receive a CLOSED frame because the device left BLE
|
||||
* range while the lid was open — otherwise the overlay lingers until manually dismissed (one of
|
||||
* the symptoms in #598). A ticker re-checks the primary device's freshness; once a previously
|
||||
* fresh OPEN broadcast goes stale past [CASE_OPEN_STALE_TIMEOUT] (no newer advertisement, or the
|
||||
* device dropped to cache-only), a single Hide is emitted. The lid-driven [monitorCase] still
|
||||
* handles the normal close; a redundant Hide here is harmless ([PopUpWindow.close] is idempotent).
|
||||
*/
|
||||
private fun monitorCaseStaleClose(): Flow<Event> = combine(
|
||||
deviceMonitor.primaryDevice(),
|
||||
staleCheckTicker(),
|
||||
) { device, _ -> isCaseOpenBroadcastFresh(device) }
|
||||
.distinctUntilChanged()
|
||||
.withPrevious()
|
||||
.mapNotNull { (wasFresh, isFresh) ->
|
||||
if (wasFresh == true && !isFresh) {
|
||||
log(TAG) { "Case-open broadcast went stale, emitting Hide" }
|
||||
Event.PopupHide(timeSource.now())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "popUpCaseStale" }
|
||||
|
||||
/** True while the primary device is eligible and currently advertising a fresh OPEN lid. */
|
||||
internal fun isCaseOpenBroadcastFresh(device: PodDevice?): Boolean {
|
||||
if (device?.reactions?.showPopUpOnCaseOpen != true) return false
|
||||
if (device.caseLidState != DualApplePods.LidState.OPEN) return false
|
||||
// Track BLE freshness specifically, not PodDevice.seenLastAt (which also counts AAP/cache):
|
||||
// the lid is a BLE-only signal, so a live AAP socket must not keep a stale OPEN on screen.
|
||||
val bleSeenLastAt = device.ble?.seenLastAt ?: return false
|
||||
return Duration.between(bleSeenLastAt, timeSource.now()) <= CASE_OPEN_STALE_TIMEOUT
|
||||
}
|
||||
|
||||
private fun staleCheckTicker(): Flow<Unit> = flow {
|
||||
while (true) {
|
||||
emit(Unit)
|
||||
delay(STALE_CHECK_INTERVAL.toMillis())
|
||||
}
|
||||
}
|
||||
|
||||
fun monitor(): Flow<Event> = merge(monitorCase(), monitorConnection(), monitorCaseStaleClose())
|
||||
|
||||
sealed class Event {
|
||||
data class PopupShow(
|
||||
@@ -296,5 +344,11 @@ class PopUpReaction @Inject constructor(
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Reaction", "PopUp")
|
||||
|
||||
/** A case-open popup is force-dismissed once its OPEN broadcast hasn't refreshed for this long. */
|
||||
private val CASE_OPEN_STALE_TIMEOUT: Duration = Duration.ofSeconds(4)
|
||||
|
||||
/** How often the stale-close backstop re-evaluates freshness. */
|
||||
private val STALE_CHECK_INTERVAL: Duration = Duration.ofSeconds(2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.apple.ble.getBatteryIcon
|
||||
import eu.darken.capod.pods.core.apple.ble.toBatteryFloat
|
||||
import eu.darken.capod.pods.core.apple.ble.toBatteryOrNull
|
||||
|
||||
@Composable
|
||||
fun PopUpContent(
|
||||
@@ -115,7 +113,7 @@ private fun DualPodContent(device: PodDevice) {
|
||||
// Left pod
|
||||
BatteryColumn(
|
||||
iconRes = device.leftPodIcon,
|
||||
batteryPercent = device.batteryLeft.toBatteryFloat(),
|
||||
batteryPercent = device.batteryLeft,
|
||||
isCharging = device.isLeftPodCharging ?: false,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -124,7 +122,7 @@ private fun DualPodContent(device: PodDevice) {
|
||||
if (device.hasCase) {
|
||||
BatteryColumn(
|
||||
iconRes = device.caseIcon,
|
||||
batteryPercent = device.batteryCase.toBatteryFloat(),
|
||||
batteryPercent = device.batteryCase,
|
||||
isCharging = device.isCaseCharging ?: false,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -133,7 +131,7 @@ private fun DualPodContent(device: PodDevice) {
|
||||
// Right pod
|
||||
BatteryColumn(
|
||||
iconRes = device.rightPodIcon,
|
||||
batteryPercent = device.batteryRight.toBatteryFloat(),
|
||||
batteryPercent = device.batteryRight,
|
||||
isCharging = device.isRightPodCharging ?: false,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -144,7 +142,7 @@ private fun DualPodContent(device: PodDevice) {
|
||||
private fun SinglePodContent(device: PodDevice) {
|
||||
BatteryColumn(
|
||||
iconRes = device.iconRes,
|
||||
batteryPercent = device.batteryHeadset.toBatteryFloat(),
|
||||
batteryPercent = device.batteryHeadset,
|
||||
isCharging = device.isHeadsetBeingCharged ?: false,
|
||||
)
|
||||
}
|
||||
@@ -157,7 +155,6 @@ private fun BatteryColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val nullablePercent = batteryPercent.toBatteryOrNull()
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
@@ -184,14 +181,14 @@ private fun BatteryColumn(
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = getBatteryIcon(nullablePercent),
|
||||
imageVector = getBatteryIcon(batteryPercent),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = formatBatteryPercent(context, nullablePercent),
|
||||
text = formatBatteryPercent(context, batteryPercent),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -8,8 +8,8 @@ import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
@@ -23,14 +23,13 @@ import eu.darken.capod.pods.core.unknown.UnknownSnapshotBle
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -43,12 +42,14 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
private val blePodMonitor: BlePodMonitor,
|
||||
private val bleScanModeController: BleScanModeController,
|
||||
private val deviceMonitor: DeviceMonitor,
|
||||
private val debugSettings: DebugSettings,
|
||||
private val timeSource: TimeSource,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
private val _bleState = MutableStateFlow<BleState>(BleState.Intro())
|
||||
|
||||
/** Guards against re-entrant runs (e.g. a double tap on "Try again"). */
|
||||
private val runLock = Mutex()
|
||||
|
||||
data class State(val bleState: BleState)
|
||||
|
||||
val state = _bleState.map { State(it) }.asLiveState()
|
||||
@@ -84,166 +85,184 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun troubleShootBle() = launch(context = dispatcherProvider.IO) {
|
||||
if (!runLock.tryLock()) {
|
||||
log(TAG, WARN) { "troubleShootBle() ignored, a run is already in progress" }
|
||||
return@launch
|
||||
}
|
||||
log(TAG, INFO) { "troubleShootBle()" }
|
||||
|
||||
bleScanModeController.withTemporaryOverride(ScannerMode.LOW_LATENCY) override@{
|
||||
run {
|
||||
progress("Checking for headphones...")
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
deviceMonitor.primaryDevice().filterNotNull().firstOrNull()
|
||||
}
|
||||
if (mainDevice != null) {
|
||||
success("Headphones found, nothing to troubleshoot.")
|
||||
return@override
|
||||
} else {
|
||||
progress("Headphones not detected.\n")
|
||||
}
|
||||
}
|
||||
try {
|
||||
bleScanModeController.withTemporaryOverride(ScannerMode.LOW_LATENCY) override@{
|
||||
// The combo under which supported headphones were detected (set in sweep 2).
|
||||
var supportedCombo: BlePodMonitor.CompatOverride? = null
|
||||
// Set only when the run reaches a terminal success. Persisted on exit; staying null
|
||||
// (any failure, or cancellation) means we just clear the override, which restores the
|
||||
// user's original — never-touched — settings.
|
||||
var comboToPersist: BlePodMonitor.CompatOverride? = null
|
||||
try {
|
||||
run {
|
||||
progress("Checking for headphones...")
|
||||
if (findLiveBleHeadphones()) {
|
||||
success("Headphones found, nothing to troubleshoot.")
|
||||
return@override
|
||||
} else {
|
||||
progress("Headphones not detected.\n")
|
||||
}
|
||||
}
|
||||
|
||||
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<BlePodSnapshot> =
|
||||
{ hardwareFilteringDisabled,
|
||||
hardwareBatchingDisabled,
|
||||
indirectCallback,
|
||||
unfiltered ->
|
||||
val sb = StringBuilder("SCAN - Settings: ")
|
||||
sb.append("hardwareFilteringDisabled=$hardwareFilteringDisabled, ")
|
||||
sb.append("hardwareBatchingDisabled=$hardwareBatchingDisabled, ")
|
||||
sb.append("indirectCallback=$indirectCallback, ")
|
||||
sb.append("unfiltered=$unfiltered")
|
||||
progress(sb.toString())
|
||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = hardwareFilteringDisabled
|
||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = hardwareBatchingDisabled
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = indirectCallback
|
||||
debugSettings.showUnfiltered.valueBlocking = unfiltered
|
||||
val doScan: suspend (BlePodMonitor.CompatOverride, Boolean) -> Collection<BlePodSnapshot> =
|
||||
{ combo, unfiltered ->
|
||||
progress(
|
||||
"SCAN - Settings: filteringDisabled=${combo.offloadedFilteringDisabled}, " +
|
||||
"batchingDisabled=${combo.offloadedBatchingDisabled}, " +
|
||||
"indirectCallback=${combo.indirectCallback}, unfiltered=$unfiltered"
|
||||
)
|
||||
blePodMonitor.setCompatOverride(combo)
|
||||
blePodMonitor.setUnfilteredOverride(unfiltered)
|
||||
val devices = collectFreshDevices()
|
||||
log(TAG) { "SCAN: Fresh BLE devices: $devices" }
|
||||
if (devices.isNotEmpty()) {
|
||||
progress("SCAN: Received data from ${devices.size} BLE devices")
|
||||
} else {
|
||||
progress("SCAN: No data received")
|
||||
}
|
||||
devices
|
||||
}
|
||||
|
||||
val start = timeSource.elapsedRealtime()
|
||||
val devices = withTimeoutOrNull(STEP_TIME) {
|
||||
blePodMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { timeSource.elapsedRealtime() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
log(TAG) { "SCAN: BLE Devices: $devices" }
|
||||
if (devices.isNotEmpty()) {
|
||||
progress("SCAN: Received data from ${devices.size} BLE devices")
|
||||
devices
|
||||
} else {
|
||||
progress("SCAN: No data received")
|
||||
devices
|
||||
run {
|
||||
progress("Checking if we can receive BLE data at all.")
|
||||
val gotData = COMPAT_COMBOS.any { combo -> doScan(combo, true).isNotEmpty() }
|
||||
if (!gotData) {
|
||||
failure("Phone is not receiving BLE data.", BleState.Result.Failure.Type.PHONE)
|
||||
return@override
|
||||
}
|
||||
}
|
||||
|
||||
progress("We received at least some BLE data.\n")
|
||||
|
||||
run {
|
||||
progress("Checking for supported headphones.")
|
||||
supportedCombo = COMPAT_COMBOS.firstOrNull { combo ->
|
||||
doScan(combo, false).any { it !is UnknownSnapshotBle }
|
||||
}
|
||||
if (supportedCombo == null) {
|
||||
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
|
||||
return@override
|
||||
}
|
||||
}
|
||||
|
||||
progress("Found some headphones that are supported by CAPod.\n")
|
||||
|
||||
run {
|
||||
progress("Checking for your headphones with new BLE settings...")
|
||||
if (findLiveBleHeadphones()) {
|
||||
comboToPersist = supportedCombo
|
||||
success("Found your headphones, new BLE settings worked :)!")
|
||||
return@override
|
||||
}
|
||||
}
|
||||
|
||||
progress("Still no headphones detected that count as yours.\n")
|
||||
|
||||
run {
|
||||
progress("Checking all closeby headphones.")
|
||||
|
||||
val otherDevices = collectFreshDevices()
|
||||
otherDevices.forEachIndexed { index, dev -> log(TAG) { "Device #$index: $dev" } }
|
||||
|
||||
val candidate = otherDevices
|
||||
.filter { it !is UnknownSnapshotBle }
|
||||
.maxByOrNull { it.signalQuality }
|
||||
|
||||
if (candidate == null) {
|
||||
failure(
|
||||
"No supported headphones found near your device.",
|
||||
BleState.Result.Failure.Type.HEADPHONES,
|
||||
)
|
||||
return@override
|
||||
}
|
||||
|
||||
progress("Headphones found nearby, but not detected as yours.\n")
|
||||
progress("Creating profile for closest headphones.")
|
||||
log(TAG, INFO) { "Candidate is $candidate" }
|
||||
|
||||
profilesRepo.addProfile(
|
||||
profile = AppleDeviceProfile(
|
||||
label = context.getString(R.string.troubleshooter_title),
|
||||
model = candidate.model,
|
||||
),
|
||||
addFirst = true,
|
||||
)
|
||||
|
||||
if (findLiveBleHeadphones()) {
|
||||
comboToPersist = supportedCombo
|
||||
success("Success! Detected your headphones.")
|
||||
} else {
|
||||
failure("No headphones detected near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
blePodMonitor.setUnfilteredOverride(false)
|
||||
try {
|
||||
// Persist the winning combo before dropping the override so the effective scan
|
||||
// settings stay equal with no restart flicker. Done in its own try so the
|
||||
// override is still cleared (restoring originals) even if a write throws.
|
||||
comboToPersist?.let { persistCompat(it) }
|
||||
} finally {
|
||||
blePodMonitor.setCompatOverride(null)
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
progress("Checking if we can receive BLE data at all.")
|
||||
if (doScan(false, false, false, true).isNotEmpty()) return@run
|
||||
if (doScan(false, false, true, true).isNotEmpty()) return@run
|
||||
if (doScan(true, true, true, true).isNotEmpty()) return@run
|
||||
if (doScan(true, true, false, true).isNotEmpty()) return@run
|
||||
if (doScan(true, false, true, true).isNotEmpty()) return@run
|
||||
if (doScan(true, false, false, true).isNotEmpty()) return@run
|
||||
if (doScan(false, true, true, true).isNotEmpty()) return@run
|
||||
if (doScan(false, true, false, true).isNotEmpty()) return@run
|
||||
|
||||
failure("Phone is not receiving BLE data.", BleState.Result.Failure.Type.PHONE)
|
||||
|
||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = false
|
||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = false
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = false
|
||||
debugSettings.showUnfiltered.valueBlocking = false
|
||||
|
||||
return@override
|
||||
}
|
||||
|
||||
progress("We received at least some BLE data.\n")
|
||||
|
||||
run {
|
||||
progress("Checking for supported headphones.")
|
||||
|
||||
if (doScan(false, false, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(false, false, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, true, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, true, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, false, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, false, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(false, true, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(false, true, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
|
||||
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
|
||||
|
||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = false
|
||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = false
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = false
|
||||
|
||||
return@override
|
||||
}
|
||||
|
||||
progress("Found some headphones that are supported by CAPod.\n")
|
||||
|
||||
run {
|
||||
progress("Checking for your headphones with new BLE settings...")
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
deviceMonitor.primaryDevice().filterNotNull().firstOrNull()
|
||||
}
|
||||
if (mainDevice != null) {
|
||||
success("Found your headphones, new BLE settings worked :)!")
|
||||
return@override
|
||||
}
|
||||
}
|
||||
|
||||
progress("Still no headphones detected that count as yours.\n")
|
||||
|
||||
run {
|
||||
progress("Checking all closeby headphones.")
|
||||
|
||||
val otherDevices = withTimeoutOrNull(STEP_TIME) {
|
||||
val start = timeSource.elapsedRealtime()
|
||||
blePodMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { timeSource.elapsedRealtime() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
|
||||
otherDevices.forEachIndexed { index, dev -> log(TAG) { "Device #$index: $dev" } }
|
||||
|
||||
if (otherDevices.isEmpty()) {
|
||||
failure("No supported headphones found near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
return@override
|
||||
}
|
||||
|
||||
progress("Headphones found nearby, but not detected as yours.\n")
|
||||
progress("Creating profile for closest headphones.")
|
||||
|
||||
val candidate = otherDevices
|
||||
.filter { it !is UnknownSnapshotBle }
|
||||
.maxBy { it.signalQuality }
|
||||
|
||||
log(TAG, INFO) { "Candidate is $candidate" }
|
||||
|
||||
profilesRepo.addProfile(
|
||||
profile = AppleDeviceProfile(
|
||||
label = context.getString(R.string.troubleshooter_title),
|
||||
model = candidate.model,
|
||||
),
|
||||
addFirst = true,
|
||||
)
|
||||
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
deviceMonitor.primaryDevice().filterNotNull().firstOrNull()
|
||||
}
|
||||
|
||||
if (mainDevice != null) {
|
||||
success("Success! Detected your headphones.")
|
||||
} else {
|
||||
failure("No headphones detected near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
runLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits up to [STEP_TIME] for the primary profile to be backed by a *fresh, live BLE*
|
||||
* observation. A cached-only / AAP-only primary does not count — the troubleshooter is about
|
||||
* whether BLE advertisements are actually reaching us. The freshness cutoff (snapshot seen at or
|
||||
* after this call) means it reflects the currently-active scan settings and doesn't rely on the
|
||||
* device cache having been cleared beforehand.
|
||||
*/
|
||||
private suspend fun findLiveBleHeadphones(): Boolean {
|
||||
val threshold = timeSource.now()
|
||||
return withTimeoutOrNull(STEP_TIME) {
|
||||
deviceMonitor.primaryDevice().firstOrNull { device ->
|
||||
device?.ble != null && (device.seenLastAt?.let { it >= threshold } == true)
|
||||
} != null
|
||||
} ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects BLE devices observed *after the current scan settings take effect*. Option changes
|
||||
* restart the scan after a throttle, and [BlePodMonitor] keeps a 20s device cache, so without a
|
||||
* freshness cutoff a stale observation from a previous combo could be mistaken for a "win".
|
||||
*/
|
||||
private suspend fun collectFreshDevices(): List<BlePodSnapshot> {
|
||||
// Drop anything cached under a previous combo so it can't satisfy this attempt.
|
||||
blePodMonitor.clearDeviceCache()
|
||||
val freshThreshold = timeSource.now().plusMillis(SCAN_SETTLE_MS)
|
||||
val start = timeSource.elapsedRealtime()
|
||||
val collected = mutableListOf<BlePodSnapshot>()
|
||||
// Accumulate as we go: a timeout must not discard what we already observed. toList() only
|
||||
// returns once the flow completes, which a quiet channel may never do within the window.
|
||||
withTimeoutOrNull(STEP_TIME) {
|
||||
blePodMonitor.devices
|
||||
.takeWhile { timeSource.elapsedRealtime() - start < STEP_TIME - 1000 }
|
||||
.collect { snapshots -> collected.addAll(snapshots) }
|
||||
}
|
||||
return collected
|
||||
.filter { it.seenLastAt >= freshThreshold }
|
||||
.distinctBy { it.address }
|
||||
}
|
||||
|
||||
private fun persistCompat(combo: BlePodMonitor.CompatOverride) {
|
||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = combo.offloadedFilteringDisabled
|
||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = combo.offloadedBatchingDisabled
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = combo.indirectCallback
|
||||
}
|
||||
|
||||
sealed class BleState {
|
||||
class Intro : BleState()
|
||||
|
||||
@@ -294,6 +313,29 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
companion object {
|
||||
const val STEP_TIME = 10 * 1000L
|
||||
|
||||
/**
|
||||
* Grace period after switching compat settings before an observation counts as "fresh".
|
||||
* Covers [BlePodMonitor]'s ~1s scan-option throttle plus the scanner restart.
|
||||
*/
|
||||
const val SCAN_SETTLE_MS = 1500L
|
||||
|
||||
/**
|
||||
* Compatibility combinations to probe, ordered fewest-disables-first so the first one that
|
||||
* works (and gets persisted) is the *minimal* set of overrides — e.g. a phone that only
|
||||
* needs batching disabled won't also get filtering disabled. Triple semantics:
|
||||
* (offloadedFilteringDisabled, offloadedBatchingDisabled, indirectCallback).
|
||||
*/
|
||||
val COMPAT_COMBOS: List<BlePodMonitor.CompatOverride> = listOf(
|
||||
BlePodMonitor.CompatOverride(false, false, false), // baseline (no overrides)
|
||||
BlePodMonitor.CompatOverride(false, true, false), // batching only
|
||||
BlePodMonitor.CompatOverride(true, false, false), // filtering only
|
||||
BlePodMonitor.CompatOverride(false, false, true), // indirect callback only
|
||||
BlePodMonitor.CompatOverride(false, true, true), // batching + indirect
|
||||
BlePodMonitor.CompatOverride(true, false, true), // filtering + indirect
|
||||
BlePodMonitor.CompatOverride(true, true, false), // filtering + batching
|
||||
BlePodMonitor.CompatOverride(true, true, true), // everything
|
||||
)
|
||||
|
||||
val TAG = logTag("TroubleShooter", "VM")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">As u onlangs gekoop het, kan dit \'n oomblik neem vir Google Play om te sinkroniseer.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Probeer weer oor \'n paar minute as u aankoop nie verskyn nie.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Maak seker dat u met dieselfde Google-rekening aangemeld is wat vir die aankoop gebruik is.</string>
|
||||
<string name="settings_monitor_mode_label">Monitormodus</string>
|
||||
<string name="settings_monitor_mode_description">Onder watter omstandighede hierdie toepassing Bluetooth-data monitor.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Ekstra kennisgewing</string>
|
||||
<string name="settings_monitor_connected_notification_description">Wys \'n ekstra kennisgewing wanneer \'n toestel gekoppel is. Dit laat jou toe om die permanente \"Geen toestelle\" kennisgewing te versteek deur die \"Toestelstatus\" kanaal te deaktiveer.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Hou kennisgewing ná ontkoppeling</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Outo-pouse</string>
|
||||
<string name="settings_autopause_description">Pouseer oudio wanneer die toestel uit jou oor verwyder word.</string>
|
||||
<string name="settings_autopplay_label">Outo-speel</string>
|
||||
<string name="settings_autoplay_description">Begin oudio-terugspeel wanneer toestel gedra word.</string>
|
||||
<string name="settings_autoplay_description">Hervat musiek wanneer jy jou pods weer dra, maar slegs as Outopouseer dit gestop het. Pouses wat jy self geaktiveer het (foon, AirPods-steel, slaap) bly gepouseer.</string>
|
||||
<string name="settings_start_music_on_wear_label">Begin musiek by dra</string>
|
||||
<string name="settings_start_music_on_wear_description">Probeer altyd musiek speel wanneer jy jou pods opsit, selfs na \'n handmatige pouseer.</string>
|
||||
<string name="settings_eardetection_info_label">Nota oor oorbespeuring</string>
|
||||
<string name="settings_eardetection_info_description">As oorbespeuring net vir een pod werk, is dit \'n Apple-beperking. Slegs die primêre pod (wat vir die mikrofoon gebruik word) word bespeur. Stel op Apple-toestelle op: Settings → Bluetooth → AirPods → Microphone.</string>
|
||||
<string name="settings_fake_data_label">Vals data</string>
|
||||
<string name="settings_fake_data_description">Wys vals data, d.w.s. simuleer toestelle wat nie bestaan nie.</string>
|
||||
<string name="settings_debug_label">Ontfoutinstellings</string>
|
||||
<string name="settings_debug_description">Addisionele instellings om probleme met die toepassing te help oplos.</string>
|
||||
<string name="settings_signal_minimum_label">Minimum seingehalte</string>
|
||||
<string name="settings_signal_minimum_description">Die minimum seingehalte wat \'n toestel moet hê om as joune beskou te word.</string>
|
||||
<string name="settings_autoconnect_label">Outo-koppel</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Wys verbindingsopspringer</string>
|
||||
<string name="settings_popup_connected_description">Wys \'n opspringer wanneer die toestel vir die eerste keer koppel.</string>
|
||||
<string name="settings_popup_info_not_in_app">Opspringvensters word slegs gewys as jy nie in die program is nie.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Opspringvensters benodig die monitor om te loop. Jou monitormodus is ingestel op \"Wanneer program oop is\", wat die monitor stop wanneer jy die program verlaat.</string>
|
||||
<string name="general_fix_it_action">Herstel dit</string>
|
||||
<string name="settings_charged_notification_label">Gelaaide kennisgewing</string>
|
||||
<string name="settings_charged_notification_description">Wys \'n kennisgewing sodra alles wat laai die teikenlaai vlak bereik.</string>
|
||||
<string name="settings_charged_threshold_label">Teikenlaai vlak</string>
|
||||
<string name="settings_charged_scope_label">Gemonitorde batterye</string>
|
||||
<string name="settings_charged_scope_pods_label">Slegs pods</string>
|
||||
<string name="settings_charged_scope_case_label">Slegs kis</string>
|
||||
<string name="settings_charged_scope_both_label">Pods en kis</string>
|
||||
<string name="device_settings_experimental_title">Eksperimentele kenmerk</string>
|
||||
<string name="device_settings_experimental_description">Hierdie kenmerk is nog nie op alle toestelle getoets nie. As iets nie korrek werk nie, open asseblief \'n kwessie met \'n foutopsporingsloglêer.</string>
|
||||
<string name="device_settings_experimental_action">Open Kwessiespoorder</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Ontfout</string>
|
||||
<string name="settings_general_label">Algemene instellings</string>
|
||||
<string name="settings_general_description">Algemene aanpassings wat die hele toepassing beïnvloed.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Versteek onooreenstemmende toestelle</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Moenie naby toestelle wys wat nie ooreenstem met enige van jou profiele in die oorsig nie, bv. ander mense se AirPods.</string>
|
||||
<string name="settings_acknowledgements_label">Erkennings</string>
|
||||
<string name="settings_debug_autoreports_label">Outomatiese foutverslae</string>
|
||||
<string name="settings_debug_autoreports_description">Rapporteer probleme outomaties, bv. besonderhede oor \'n toepassingstorting sodat ek kan uitvind hoe om dit reg te stel.</string>
|
||||
<string name="settings_debug_mode_label">Ontfoutmodus</string>
|
||||
<string name="settings_debug_mode_description">Wys addisionele inligting om probleme op te los.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Ongefiltreerde BLE-data</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Verwyder enige filters van die BLE-skandeerder om alle uitgesaaide BLE-data te wys. Nuttig om ondersteuning vir nuwe koptelefoontipes by te voeg.</string>
|
||||
<string name="settings_compatibility_mode_label">Verenigbaarheidsmodus</string>
|
||||
<string name="settings_compatibility_mode_description">Deaktiveer optimalisasies om verenigbaarheid te verbeter. Probeer dit as jy geen data sien nie.</string>
|
||||
<string name="help_translate_label">Vertaling</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Hierdie widget vereis \'n direkte verbinding (AAP) met jou toestel. Maak seker jou toestel is gepaar en verbind in die program voor jy hierdie widget gebruik.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Hierdie toestel ondersteun nie geraasbeheersing nie</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Nie verbind nie</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Naby maar nie gekoppel nie</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Nie naby nie</string>
|
||||
<string name="anc_widget_aap_connecting_label">Verbind…</string>
|
||||
<string name="tile_anc_label">Geraaskontrole</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Toestemming benodig</string>
|
||||
<string name="tile_anc_subtitle_no_device">Geen toestel</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Geen geraaskontrole</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth af</string>
|
||||
<string name="widget_config_screen_title">Widget-konfigurasie</string>
|
||||
<string name="widget_configuration_title">Kies toestel</string>
|
||||
<string name="widget_configuration_description">Kies watter toestelprofiel hierdie legstuk moet wys.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth is gedeaktiveer, aktiveer dit ;)</string>
|
||||
<string name="overview_monitoring_active_label">Moniteer vir toestelle</string>
|
||||
<string name="overview_monitoring_active_description">Maak seker jou toestel is naby en aktief.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Verbind, maar geen data nie</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Jou toestel is verbind, maar CAPod ontvang geen lewende data daarvan nie. Jou foon het dalk \'n versoenbaarheidsopsie nodig — die probleemoplosser kan probeer om een outomaties te vind.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Voer probleemoplosser uit</string>
|
||||
<string name="overview_unmatched_devices_label">Ongepasde toestelle</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d toestel sonder ooreenstemmende profiel</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Die volgende toestemming is vereis:</string>
|
||||
<string name="permission_system_alert_window_label">Stelselwaarskuwingsvenster</string>
|
||||
<string name="permission_system_alert_window_description">Laat CAPod toe om oor ander toepassings te teken om die kenmerk Wys opspringer moontlik te maak.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Wanneer toepassing oop is</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Wanneer toestel gekoppel is</string>
|
||||
<string name="settings_monitor_mode_always_label">Altyd</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Wanneer gesien</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Kassie is oop</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">In oor</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Deursigtigheid</string>
|
||||
<string name="anc_mode_adaptive">Aanpasbaar</string>
|
||||
<string name="conversation_awareness_label">Gespreksbewustheid</string>
|
||||
<string name="settings_conversation_action_label">Wanneer jy begin praat</string>
|
||||
<string name="settings_conversation_action_nothing_label">Doen niks</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Verlaag volume</string>
|
||||
<string name="settings_conversation_action_pause_label">Pouseer media</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Volumevermindering</string>
|
||||
<string name="signal_badge_ble_cd">BLE-skandering aktief</string>
|
||||
<string name="signal_badge_key_irk_cd">Identiteit geverifieer</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Versleutelde verbinding</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Amber</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Gebruik muurpapierkleure</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitering</string>
|
||||
<string name="settings_category_appearance_label">Voorkoms</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Ongestoorde veranderinge</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Algemeen</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofoon</string>
|
||||
<string name="device_settings_microphone_mode_description">Watter AirPod as mikrofoon gebruik word</string>
|
||||
<string name="device_settings_microphone_mode_description">Watter oorfoon as mikrofoon gebruik word</string>
|
||||
<string name="device_settings_microphone_mode_auto">Outomaties</string>
|
||||
<string name="device_settings_microphone_mode_right">Regs</string>
|
||||
<string name="device_settings_microphone_mode_left">Links</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Gelaaide kennisgewing</string>
|
||||
<string name="reaction_charged_notification_title">Laai voltooi</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s is volledig gelaai.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s is na %2$d%% gelaai.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">በቅርቡ ካዘዙ፣ Google Play ለማመሳሰል ትንሽ ጊዜ ሊወስድ ይችላል።</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">ግዢዎ ካልታየ ከጥቂት ደቂቃዎች በኋላ እንደገና ሞክር።</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">ለግዢው ጥቅም ላይ ከዋለው ጋር ተመሳሳይ Google መለያ ተጠቅመህ ገብተሃል ወይም ብለህ አረጋግጥ።</string>
|
||||
<string name="settings_monitor_mode_label">የክትትል ሁነታ</string>
|
||||
<string name="settings_monitor_mode_description">ይህ መተግበሪያ የብሉቱዝ መረጃን የሚከታተልበት ሁኔታዎች።</string>
|
||||
<string name="settings_monitor_connected_notification_label">ተጨማሪ ማሳወቂያ</string>
|
||||
<string name="settings_monitor_connected_notification_description">አንድ መሣሪያ ሲገናኝ ተጨማሪ ማሳወቂያ ያሳያል። ይህ የ \"የመሣሪያ ሁኔታ\" ቻናልን በማሰናከል ቋሚውን \"ምንም መሣሪያዎች የሉም\" የሚለውን ማሳወቂያ እንዲደብቁ ያስችልዎታል።</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">ከተነተቀ በኋላ ማሳወቂያውን ይያዙ</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">ራስ-ሰር ለአፍታ ማቆም</string>
|
||||
<string name="settings_autopause_description">መሳሪያውን ከጆሮዎ ሲያወጡ ድምጽን ለአፍታ ያቁሙ።</string>
|
||||
<string name="settings_autopplay_label">ራስ-ሰር አጫውት</string>
|
||||
<string name="settings_autoplay_description">መሳሪያው ሲለበስ የድምጽ መልሶ ማጫወት ይጀምሩ።</string>
|
||||
<string name="settings_autoplay_description">ፖዶቹን እንደገና ሲያጠቁ ሙዚቃን ቀጥል፣ ግን ራስ-አቆም ካቆመው ብቻ። ራስዎ ያቆሙት (ስልክ፣ የ AirPods ቀጭኑ፣ ወይም እንቅልፍ) ቆሞ ይቆያል።</string>
|
||||
<string name="settings_start_music_on_wear_label">ሲያጠቁ ሙዚቃ ጀምር</string>
|
||||
<string name="settings_start_music_on_wear_description">ፖዶቹን ሲያጠቁ ሁልጊዜ ሙዚቃ ለማጫወት ይሞክራል፣ እጅ ሲቆም ካቆሙ በኋላ እንኳን።</string>
|
||||
<string name="settings_eardetection_info_label">የጆሮ ማወቂያ ማስታወሻ</string>
|
||||
<string name="settings_eardetection_info_description">የጆሮ ማወቂያ ለአንድ ፖድ ብቻ የሚሰራ ከሆነ ይህ የApple ገደብ ነው። \"ዋና ፖድ\" (ለማይክሮፎን የሚጠቅም) ብቻ ይታወቃል። በApple መሳሪያዎች ላይ ያዋቅሩ: Settings → Bluetooth → AirPods → Microphone.</string>
|
||||
<string name="settings_fake_data_label">የውሸት መረጃ</string>
|
||||
<string name="settings_fake_data_description">የውሸት መረጃ ያሳዩ፣ ማለትም የሌሉ መሳሪያዎችን አስመስለው ያሳዩ።</string>
|
||||
<string name="settings_debug_label">የማረሚያ ቅንብሮች</string>
|
||||
<string name="settings_debug_description">በመተግበሪያው ላይ ያሉ ችግሮችን ለመፍታት የሚረዱ ተጨማሪ ቅንብሮች።</string>
|
||||
<string name="settings_signal_minimum_label">ዝቅተኛ የሲግናል ጥራት</string>
|
||||
<string name="settings_signal_minimum_description">አንድ መሣሪያ የእርስዎ ተደርጎ እንዲቆጠር ሊኖረው የሚገባው ዝቅተኛው የሲግናል ጥራት።</string>
|
||||
<string name="settings_autoconnect_label">ራስ-ሰር ግንኙነት</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">የግንኙነት ብቅ-ባይ አሳይ</string>
|
||||
<string name="settings_popup_connected_description">መሳሪያው ለመጀመሪያ ጊዜ ሲገናኝ ብቅ-ባይ አሳይ።</string>
|
||||
<string name="settings_popup_info_not_in_app">ፖፕ-አፕ መልዕክቶች በፕሮግራሙ ውስጥ ሳታሉ ብቻ ይታያሉ።</string>
|
||||
<string name="settings_popup_warning_manual_mode">ፖፕ-አፕ መልዕክቶች ሞኒተሩ እየሮጠ እንዲሆን ይፈልጋሉ። የሞኒተር ሁነታዎ \"ፕሮግራሙ ሲከፈት\" ተዘጋጅቷል፣ ይህም ፕሮግራሙን ሲለቁ ሞኒተሩን ያቆማል።</string>
|
||||
<string name="general_fix_it_action">አስተካክለው</string>
|
||||
<string name="settings_charged_notification_label">የተሞላ ማሳወቂያ</string>
|
||||
<string name="settings_charged_notification_description">ሁሉም ነገር ወደ ታለመው የኃይል ደረጃ ሲደርስ ማሳወቂያ አሳይ።</string>
|
||||
<string name="settings_charged_threshold_label">ታለመው የኃይል ደረጃ</string>
|
||||
<string name="settings_charged_scope_label">የሚታዩ ባትሪዎች</string>
|
||||
<string name="settings_charged_scope_pods_label">ፖድስ ብቻ</string>
|
||||
<string name="settings_charged_scope_case_label">ኬዝ ብቻ</string>
|
||||
<string name="settings_charged_scope_both_label">ፖድስ እና ኬዝ</string>
|
||||
<string name="device_settings_experimental_title">ሙከራዊ ባህሪ</string>
|
||||
<string name="device_settings_experimental_description">ይህ ባህሪ በሁሉም መሳሪያዎች ላይ ገና አልተሞከረም። ሆኖም ካልሰራ፣ እባክዎ የዲበግ ምዝግብ ማስታወሻ ጨምረው ጉዳዩን ይክፈቱ።</string>
|
||||
<string name="device_settings_experimental_action">የጉዳይ መከታተያ ክፈት</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">ዲባግ</string>
|
||||
<string name="settings_general_label">ቅንብሮች</string>
|
||||
<string name="settings_general_description">ሙሉውን መተግበሪያ የሚነኩ አጠቃላይ ማስተካከያዎች።</string>
|
||||
<string name="settings_overview_hide_unmatched_label">ያልተዛመዱ መሳሪያዎችን ደብቅ</string>
|
||||
<string name="settings_overview_hide_unmatched_description">ከፕሮፋይሎቻቸው ጋር የማይዛመዱ አቅራቢያ ያሉ መሳሪያዎችን በዝርዝሩ ውስጥ አታሳይ፣ ለምሳሌ የሌሎች ሰዎች AirPods።</string>
|
||||
<string name="settings_acknowledgements_label">ምስጋናዎች</string>
|
||||
<string name="settings_debug_autoreports_label">ራስ-ሰር የሳንካ ሪፖርቶች</string>
|
||||
<string name="settings_debug_autoreports_description">ችግሮችን በራስ-ሰር ሪፖርት ያደርጋል፣ ለምሳሌ የመተግበሪያ ብልሽት ዝርዝሮችን እንዴት ማስተካከል እንዳለብኝ እንድገነዘብ።</string>
|
||||
<string name="settings_debug_mode_label">የማረሚያ ሁነታ</string>
|
||||
<string name="settings_debug_mode_description">ችግሮችን ለመፍታት ተጨማሪ መረጃ ያሳዩ።</string>
|
||||
<string name="settings_blescanner_unfiltered_label">ያልተጣራ የBLE መረጃ</string>
|
||||
<string name="settings_blescanner_unfiltered_description">ሁሉንም የተላለፉ የBLE መረጃዎችን ለማሳየት ማንኛውንም ማጣሪያ ከBLE ቃኚው ያስወግዱ። ለአዲስ የጆሮ ማዳመጫ አይነቶች ድጋፍ ለመጨመር ጠቃሚ ነው።</string>
|
||||
<string name="settings_compatibility_mode_label">የተኳኋኝነት ሁነታ</string>
|
||||
<string name="settings_compatibility_mode_description">ተኳኋኝነትን ለማሻሻል ማመቻቸቶችን አሰናክል። ምንም መረጃ ካላዩ ይህንን ይሞክሩ።</string>
|
||||
<string name="help_translate_label">ትርጉም</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">ይህ ዊጄት ወደ መሳሪያዎ ቀጥታ ግንኙነት (AAP) ይፈልጋል። ይህን ዊጄት ከመጠቀምዎ በፊት መሳሪያዎ ጥምር ሆኖ በአፕ ውስጥ መገናኘቱን ያረጋግጡ።</string>
|
||||
<string name="anc_widget_no_anc_support_label">ይህ መሳሪያ የጫጫታ ቁጥጥርን አይደግፍም</string>
|
||||
<string name="anc_widget_aap_not_connected_label">አልተገናኘም</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">አቅራቢያ ነው ግን አልተገናኘም</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">አቅራቢያ የለም</string>
|
||||
<string name="anc_widget_aap_connecting_label">በመገናኘት ላይ…</string>
|
||||
<string name="tile_anc_label">ጫጫታ ቁጥጥር</string>
|
||||
<string name="tile_anc_subtitle_permission_required">ፈቃድ ያስፈልጋል</string>
|
||||
<string name="tile_anc_subtitle_no_device">መሣሪያ የለም</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">ጫጫታ ቁጥጥር የለም</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">ብሉቱዝ ጠፍቷል</string>
|
||||
<string name="widget_config_screen_title">ዊጀት ውቅር</string>
|
||||
<string name="widget_configuration_title">መሣሪያ ይምረጡ</string>
|
||||
<string name="widget_configuration_description">ይህ መግብር የትኛውን የመሣሪያ መገለጫ ማሳየት እንዳለበት ይምረጡ።</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth ተሰናክሏል፣ ያንቁት ;)</string>
|
||||
<string name="overview_monitoring_active_label">መሣሪያዎችን በመከታተል ላይ</string>
|
||||
<string name="overview_monitoring_active_description">መሣሪያዎ ቅርብ እና ንቁ መሆኑን ያረጋግጡ።</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">ተገናኝቷል፣ ግን ምንም ዳታ የለም</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">መሳሪያህ ተገናኝቷል፣ ግን CAPod ከእሱ ምንም ቀጥታ ዳታ አይቀበልም። ስልክህ የተኳሃኝነት አማራጭ ሊፈልግ ይችላል — ችግር ፈቺው በራስ-ሰር ለማግኘት ሊሞክር ይችላል።</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">ችግር ፈቺን አስጀምር</string>
|
||||
<string name="overview_unmatched_devices_label">ያልተዛመዱ መሣሪያዎች</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">ተዛማጅ መገለጫ የሌለው %d መሣሪያ</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">የሚከተለው ፈቃድ ያስፈልጋል:</string>
|
||||
<string name="permission_system_alert_window_label">የስርዓት ማስጠንቀቂያ መስኮት</string>
|
||||
<string name="permission_system_alert_window_description">\"ብቅ-ባይ አሳይ\" ባህሪውን ለማስቻል CAPod በሌሎች መተግበሪያዎች ላይ እንዲሳል ፍቀድ።</string>
|
||||
<string name="settings_monitor_mode_manual_label">መተግበሪያው ሲከፈት</string>
|
||||
<string name="settings_monitor_mode_automatic_label">መሣሪያ ሲገናኝ</string>
|
||||
<string name="settings_monitor_mode_always_label">ሁልጊዜ</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">ሲታይ</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">ኬዝ ክፍት ነው</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">በጆሮ ውስጥ</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">ግልጽነት</string>
|
||||
<string name="anc_mode_adaptive">መላሚ</string>
|
||||
<string name="conversation_awareness_label">የውይይት ግንዛቤ</string>
|
||||
<string name="settings_conversation_action_label">መናገር ሲጀምሩ</string>
|
||||
<string name="settings_conversation_action_nothing_label">ምንም አታድርግ</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">ድምፅ ዝቅ አድርግ</string>
|
||||
<string name="settings_conversation_action_pause_label">ሚዲያ አቆም</string>
|
||||
<string name="settings_conversation_volume_reduction_label">የድምፅ ቅናሽ</string>
|
||||
<string name="signal_badge_ble_cd">BLE ቅኝት ንቁ ነው</string>
|
||||
<string name="signal_badge_key_irk_cd">ማንነት ተረጋግጧል</string>
|
||||
<string name="signal_badge_key_encrypted_cd">ምስጢራዊ ግንኙነት</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">ህብረ-ቀለማዊ</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">የዳሰሳ ምስል ቀለሞችን ይጠቀማል</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">ክትትል</string>
|
||||
<string name="settings_category_appearance_label">ገጽታ</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">ያልተቀመጡ ለውጦች</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">አጠቃላይ</string>
|
||||
<string name="device_settings_microphone_mode_label">ማይክሮፎን</string>
|
||||
<string name="device_settings_microphone_mode_description">የትኛው AirPod እንደ ማይክሮፎን ንእ ላይ ይወላል</string>
|
||||
<string name="device_settings_microphone_mode_description">የትኛው ኢርባድ እንደ ማይክሮፎን ጥቅም ላይ እንደሚውል</string>
|
||||
<string name="device_settings_microphone_mode_auto">ራስ-ሰር</string>
|
||||
<string name="device_settings_microphone_mode_right">ቀኝ</string>
|
||||
<string name="device_settings_microphone_mode_left">ግራ</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">የተሞላ ማሳወቂያ</string>
|
||||
<string name="reaction_charged_notification_title">ኃይል መሙላት ተጠናቋል</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s ሙሉ በሙሉ ተሞልቷል።</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s ወደ %2$d%% ተሞልቷል።</string>
|
||||
<string name="device_settings_rename_label">ስም ቀይር</string>
|
||||
<string name="device_settings_rename_hint">የመሣሪያ ስም</string>
|
||||
<string name="device_settings_rename_confirm">ስም ቀይር</string>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<string name="general_copy_action">نسخ</string>
|
||||
<string name="general_thank_you_label">شكرًا لك</string>
|
||||
<string name="general_upgrade_action">ترقية</string>
|
||||
<string name="common_feature_requires_pro_msg">قم بالترقية للفتح.</string>
|
||||
<string name="common_upgrade_required_label">يتطلب ترقية</string>
|
||||
<string name="common_feature_requires_pro_msg">رقّي للفتح.</string>
|
||||
<string name="common_upgrade_required_label">يتطلب الترقية</string>
|
||||
<string name="general_donate_action">تبرع</string>
|
||||
<string name="general_check_action">تحقق</string>
|
||||
<string name="general_close_action">إغلاق</string>
|
||||
@@ -20,46 +20,42 @@
|
||||
<string name="general_example_label">مثال: %s</string>
|
||||
<string name="upgrade_capod_label">ترقية CAPod</string>
|
||||
<string name="upgrade_capod_description">احصل على ميزات إضافية وادعم المطور.</string>
|
||||
<string name="upgrade_preamble">تم تطوير CAPod من قِبل شخص واحد. الترقية تفتح ميزات إضافية وتساعد في إبقاء التطبيق حياً.</string>
|
||||
<string name="upgrade_preamble">تم تطوير تطبيق CAPod بواسطة شخص واحد. يتيح التحديث ميزات إضافية ويساعد في استمرار عمل التطبيق.</string>
|
||||
<string name="upgrade_benefit_themes">تخصيص السمة</string>
|
||||
<string name="upgrade_benefit_autoplay">تشغيل تلقائي وإيقاف مؤقت تلقائي</string>
|
||||
<string name="upgrade_benefit_popups">نافذة منبثقة عند فتح الغطاء والاتصال</string>
|
||||
<string name="upgrade_benefit_widgets">عناصر واجهة الشاشة الرئيسية</string>
|
||||
<string name="upgrade_benefit_device_settings">إعدادات الجهاز المتقدمة</string>
|
||||
<string name="upgrade_benefit_device_controls">عناصر التحكم في الجهاز — الضوضاء والعصا والمزيد</string>
|
||||
<string name="upgrade_benefit_support">دعم المطور</string>
|
||||
<string name="upgrade_benefit_disclaimer">يعتمد توفر الميزات على سماعات الرأس والجهاز الخاص بك.</string>
|
||||
<string name="upgrade_screen_options_description">نفس الميزات، بتسعير مختلف. يشمل الاشتراك تجربة مجانية ويمكن إلغاؤه في أي وقت.</string>
|
||||
<string name="upgrade_screen_subscription_trial_action">ابدأ الإصدار التجريبي المجاني</string>
|
||||
<string name="upgrade_screen_subscription_action">اشترك سنوياً</string>
|
||||
<string name="upgrade_benefit_autoplay">التشغيل التلقائي & الإيقاف التلقائي</string>
|
||||
<string name="upgrade_benefit_popups">نافذة منبثقة عند فتح العلبة & الاتصال</string>
|
||||
<string name="upgrade_benefit_widgets">عناصر الشاشة الرئيسة</string>
|
||||
<string name="upgrade_benefit_device_settings">إعدادات الجهاز المتقدّمة</string>
|
||||
<string name="upgrade_benefit_device_controls">عناصر التحكم في الجهاز — الضوضاء، & السيقان، & المزيد</string>
|
||||
<string name="upgrade_benefit_support">دعم المطوّر</string>
|
||||
<string name="upgrade_benefit_disclaimer">يعتمد توفر الميزات على سمّاعات الرأس والجهاز المستخدَم.</string>
|
||||
<string name="upgrade_screen_options_description">نفس الميزات، أسعار مختلفة. يشمل الاشتراك فترة تجريبية مجانية ويمكن إلغاؤه في أي وقت.</string>
|
||||
<string name="upgrade_screen_subscription_trial_action">بدء التجربة المجانية</string>
|
||||
<string name="upgrade_screen_subscription_action">الاشتراك سنويًا</string>
|
||||
<string name="upgrade_screen_subscription_action_hint">%s/سنة</string>
|
||||
<string name="upgrade_screen_iap_action">شراء لمرة واحدة</string>
|
||||
<string name="upgrade_screen_iap_action_hint">شراء لمرة واحدة: %s</string>
|
||||
<string name="upgrade_screen_restore_purchase_action">استعادة المُشتَرَى</string>
|
||||
<string name="upgrade_screen_iap_action">الشراء مرة واحدة</string>
|
||||
<string name="upgrade_screen_iap_action_hint">الشراء لمرة واحدة: %s</string>
|
||||
<string name="upgrade_screen_restore_purchase_action">استعادة المشتريات</string>
|
||||
<string name="upgrade_screen_restore_purchase_message">لم يتم العثور على أي مشتريات. هل تستخدم الحساب الصحيح؟</string>
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">إذا اشتريت مؤخراً، قد يستغرق Google Play لحظة للمزامنة.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">حاول مرة أخرى بعد بضع دقائق إذا لم يظهر شراؤك.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">تأكد من تسجيل دخولك بنفس حساب Google المستخدم للشراء.</string>
|
||||
<string name="settings_monitor_mode_label">وضع المراقبة</string>
|
||||
<string name="settings_monitor_mode_description">تحت أي ظروف يراقب هذا التطبيق بيانات البلوتوث.</string>
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">إذا اشتريت مؤخّرًا، فقد يستغرق الأمر بعض الوقت حتى تتم مزامنة حسابك على Google Play.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">حاول مرة أخرى بعد بضع دقائق إذا لم يظهر طلبك.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">تأكّد من تسجيل دخولك باستخدام حساب Google نفسه المستخدَم في عملية الشراء.</string>
|
||||
<string name="settings_monitor_connected_notification_label">إشعار إضافي</string>
|
||||
<string name="settings_monitor_connected_notification_description">يعرض إشعارًا إضافيًا عند توصيل جهاز. يتيح لك هذا إخفاء الإشعار الدائم \"لا توجد أجهزة\" عن طريق تعطيل قناة \"حالة الجهاز\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">الاحتفاظ بالإشعار بعد قطع الاتصال</string>
|
||||
<string name="settings_keep_notification_after_disconnect_description">الاستمرار في عرض مستويات البطارية المعروفة الأخيرة حتى بعد قطع اتصال AirPods</string>
|
||||
<string name="settings_autopause_label">إيقاف مؤقت تلقائي</string>
|
||||
<string name="settings_autopause_description">إيقاف الصوت مؤقتًا عند إزالة الجهاز من أذنك.</string>
|
||||
<string name="settings_autopause_label">إيقاف مؤقّت تلقائي</string>
|
||||
<string name="settings_autopause_description">إيقاف الصوت مؤقّتًا عند إزالة الجهاز من أذنك.</string>
|
||||
<string name="settings_autopplay_label">تشغيل تلقائي</string>
|
||||
<string name="settings_autoplay_description">بدء تشغيل الصوت عند ارتداء الجهاز.</string>
|
||||
<string name="settings_autoplay_description">استئناف الموسيقى عند ارتداء السماعات مجدّدًا، ولكن فقط إذا أوقفها الإيقاف التلقائي. الإيقافات التي شغّلتها بنفسك (الهاتف، ذراع AirPods، وضع السكون) تبقى متوقّفة.</string>
|
||||
<string name="settings_start_music_on_wear_label">تشغيل الموسيقى عند الارتداء</string>
|
||||
<string name="settings_start_music_on_wear_description">يحاول دائمًا تشغيل الموسيقى عند وضع السمّاعات، حتى بعد الإيقاف اليدوي.</string>
|
||||
<string name="settings_eardetection_info_label">ملاحظة اكتشاف الأذن</string>
|
||||
<string name="settings_eardetection_info_description">إذا كان اكتشاف الأذن يعمل لسماعة واحدة فقط، فهذا قيد من Apple. يتم اكتشاف \"السماعة الأساسية\" فقط (المستخدمة للميكروفون). قم بالتكوين على أجهزة Apple: الإعدادات → Bluetooth → AirPods → الميكروفون.</string>
|
||||
<string name="settings_fake_data_label">بيانات وهمية</string>
|
||||
<string name="settings_fake_data_description">عرض بيانات وهمية، أي محاكاة الأجهزة غير الموجودة.</string>
|
||||
<string name="settings_debug_label">إعدادات التصحيح</string>
|
||||
<string name="settings_debug_description">إعدادات إضافية للمساعدة في استكشاف المشكلات المتعلقة بالتطبيق وإصلاحها.</string>
|
||||
<string name="settings_eardetection_info_description">إذا كان اكتشاف الأذن يعمل لسماعة واحدة فقط، فهذا قيدٌ من Apple. يتم اكتشاف \"السمّاعة الأساسية\" فقط (المستخدَمة للميكروفون). اضبط على أجهزة Apple: الإعدادات → Bluetooth → AirPods → الميكروفون.</string>
|
||||
<string name="settings_signal_minimum_label">الحد الأدنى لجودة الإشارة</string>
|
||||
<string name="settings_signal_minimum_description">الحد الأدنى لجودة الإشارة التي يجب أن يتمتع بها الجهاز حتى يعتبر ملكك.</string>
|
||||
<string name="settings_signal_minimum_description">الحد الأدنى لجودة الإشارة التي يجب أن يتمتّع بها الجهاز حتى يُعدّ ملكك.</string>
|
||||
<string name="settings_autoconnect_label">اتصال تلقائي</string>
|
||||
<string name="settings_autoconnect_description">إذا لم يتصل Android تلقائيًا، فيمكننا أن نطلب منه ذلك أيضًا. سيؤدي هذا إلى ضبط إعداد وضع الشاشة على \"دائمًا\".</string>
|
||||
<string name="settings_autoconnect_description">إذا لم يتّصل Android تلقائيًا، فيمكننا أن نطلب منه ذلك أيضًا. سيؤدي هذا إلى ضبط إعداد وضع الشاشة على \"دائمًا\".</string>
|
||||
<string name="settings_autoconnect_info_android12">يعتمد الاتصال التلقائي على ميزة نظام يقيّدها Android 12 والإصدارات الأحدث. قد لا تعمل على جهازك.</string>
|
||||
<string name="settings_autoconnect_condition_label">شرط الاتصال التلقائي</string>
|
||||
<string name="settings_autoconnect_condition_description">متى يجب أن نحاول الاتصال بجهازك؟</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">إظهار نافذة منبثقة للاتصال</string>
|
||||
<string name="settings_popup_connected_description">إظهار نافذة منبثقة عند اتصال الجهاز لأول مرة.</string>
|
||||
<string name="settings_popup_info_not_in_app">تظهر النوافذ المنبثقة فقط عندما لا تكون داخل التطبيق.</string>
|
||||
<string name="settings_popup_warning_manual_mode">تتطلب النوافذ المنبثقة تشغيل المراقب. وضع المراقبة الخاص بك مضبوط على \"عند فتح التطبيق\"، مما يوقف المراقب عند مغادرة التطبيق.</string>
|
||||
<string name="general_fix_it_action">إصلاحه</string>
|
||||
<string name="settings_charged_notification_label">إشعار الشحن الكامل</string>
|
||||
<string name="settings_charged_notification_description">اعرض إشعارًا بمجرد وصول جميع الأجهزة التي يتم شحنها إلى مستوى الشحن المستهدف.</string>
|
||||
<string name="settings_charged_threshold_label">مستوى الشحن المستهدف</string>
|
||||
<string name="settings_charged_scope_label">البطاريات المُراقبة</string>
|
||||
<string name="settings_charged_scope_pods_label">السمّاعات فقط</string>
|
||||
<string name="settings_charged_scope_case_label">الحافظة فقط</string>
|
||||
<string name="settings_charged_scope_both_label">السماعات والحافظة</string>
|
||||
<string name="device_settings_experimental_title">ميزة تجريبية</string>
|
||||
<string name="device_settings_experimental_description">لم يتم اختبار هذه الميزة على جميع الأجهزة بعد. إذا كان هناك شيء لا يعمل بشكل صحيح، يرجى فتح تقرير مشكلة مع سجل تصحيح.</string>
|
||||
<string name="device_settings_experimental_action">فتح متتبع المشكلات</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">تصحيح الأخطاء</string>
|
||||
<string name="settings_general_label">الإعدادات</string>
|
||||
<string name="settings_general_description">تعديلات عامة تؤثر على التطبيق بأكمله.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">إخفاء الأجهزة غير المتطابقة</string>
|
||||
<string name="settings_overview_hide_unmatched_description">لا تعرض الأجهزة القريبة التي لا تتطابق مع أي من ملفات تعريفك في النظرة العامة، على سبيل المثال سمّاعات AirPods الخاصة بالآخرين.</string>
|
||||
<string name="settings_acknowledgements_label">شكر وتقدير</string>
|
||||
<string name="settings_debug_autoreports_label">تقارير الأخطاء التلقائية</string>
|
||||
<string name="settings_debug_autoreports_description">الإبلاغ تلقائيًا عن المشكلات، على سبيل المثال تفاصيل حول تعطل التطبيق حتى أتمكن من معرفة كيفية إصلاحه.</string>
|
||||
<string name="settings_debug_mode_label">وضع التصحيح</string>
|
||||
<string name="settings_debug_mode_description">عرض معلومات إضافية لاستكشاف المشكلات وإصلاحها.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">بيانات BLE غير المصفاة</string>
|
||||
<string name="settings_blescanner_unfiltered_description">إزالة أي عوامل تصفية من ماسح BLE لعرض جميع بيانات BLE المذاعة. مفيد لإضافة دعم لأنواع سماعات الرأس الجديدة.</string>
|
||||
<string name="settings_compatibility_mode_label">وضع التوافق</string>
|
||||
<string name="settings_compatibility_mode_description">تعطيل التحسينات لتحسين التوافق. جرب هذا إذا كنت لا ترى أي بيانات.</string>
|
||||
<string name="help_translate_label">الترجمة</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">يتطلب هذا الودجت اتصالاً مباشراً (AAP) بجهازك. تأكد من إقران جهازك وتوصيله في التطبيق قبل استخدام هذا الودجت.</string>
|
||||
<string name="anc_widget_no_anc_support_label">هذا الجهاز لا يدعم التحكم في الضوضاء</string>
|
||||
<string name="anc_widget_aap_not_connected_label">غير متصل</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">قريب ولكن غير متّصل</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">غير قريب</string>
|
||||
<string name="anc_widget_aap_connecting_label">جارٍ الاتصال…</string>
|
||||
<string name="tile_anc_label">التحكم في الضوضاء</string>
|
||||
<string name="tile_anc_subtitle_permission_required">الإذن مطلوب</string>
|
||||
<string name="tile_anc_subtitle_no_device">لا يوجد جهاز</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">لا يوجد تحكم في الضوضاء</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">البلوتوث مغلق</string>
|
||||
<string name="widget_config_screen_title">إعداد عنصر الواجهة</string>
|
||||
<string name="widget_configuration_title">اختيار الجهاز</string>
|
||||
<string name="widget_configuration_description">اختر ملف تعريف الجهاز الذي يجب أن تعرضه هذه الأداة.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">البلوتوث معطل، قم بتمكينه ;)</string>
|
||||
<string name="overview_monitoring_active_label">جارٍ مراقبة الأجهزة</string>
|
||||
<string name="overview_monitoring_active_description">تأكد من أن جهازك قريب ونشط.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">متصل، ولكن لا توجد بيانات</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">جهازك متصل، لكن تطبيق CAPod لا يستقبل أي بيانات مباشرة منه. قد يحتاج هاتفك إلى خيار توافق - يمكن لمُستكشف الأخطاء ومُصلحها محاولة إيجاد واحد تلقائيًّا.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">تشغيل مُستكشف الأخطاء ومُصلحها</string>
|
||||
<string name="overview_unmatched_devices_label">أجهزة غير متطابقة</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="zero">%d أجهزة بدون ملف تعريف مطابق</item>
|
||||
@@ -228,9 +237,6 @@
|
||||
<string name="permission_required_title">الإذن التالي مطلوب:</string>
|
||||
<string name="permission_system_alert_window_label">نافذة تنبيه النظام</string>
|
||||
<string name="permission_system_alert_window_description">السماح لـ CAPod بالرسم فوق التطبيقات الأخرى لتمكين ميزة \"إظهار النافذة المنبثقة\".</string>
|
||||
<string name="settings_monitor_mode_manual_label">عند فتح التطبيق</string>
|
||||
<string name="settings_monitor_mode_automatic_label">عند اتصال الجهاز</string>
|
||||
<string name="settings_monitor_mode_always_label">دائمًا</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">عند الرؤية</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">العلبة مفتوحة</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">في الأذن</string>
|
||||
@@ -262,6 +268,11 @@
|
||||
<string name="anc_mode_transparency">الشفافية</string>
|
||||
<string name="anc_mode_adaptive">تكيفي</string>
|
||||
<string name="conversation_awareness_label">الوعي بالمحادثة</string>
|
||||
<string name="settings_conversation_action_label">عند بدء الكلام</string>
|
||||
<string name="settings_conversation_action_nothing_label">لا تفعل شيئًا</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">خفض مستوى الصوت</string>
|
||||
<string name="settings_conversation_action_pause_label">إيقاف الوسائط</string>
|
||||
<string name="settings_conversation_volume_reduction_label">تخفيض مستوى الصوت</string>
|
||||
<string name="signal_badge_ble_cd">مسح BLE نشط</string>
|
||||
<string name="signal_badge_key_irk_cd">تم التحقق من الهوية</string>
|
||||
<string name="signal_badge_key_encrypted_cd">اتصال مشفر</string>
|
||||
@@ -330,7 +341,6 @@
|
||||
<string name="ui_theme_color_amber_label">كهرماني</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">يستخدم ألوان الخلفية</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">المراقبة</string>
|
||||
<string name="settings_category_appearance_label">المظهر</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">تغييرات غير محفوظة</string>
|
||||
@@ -490,7 +500,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">عام</string>
|
||||
<string name="device_settings_microphone_mode_label">ميكروفون</string>
|
||||
<string name="device_settings_microphone_mode_description">أي AirPod يُستخدم كميكروفون</string>
|
||||
<string name="device_settings_microphone_mode_description">سمّاعة الأذن المستخدمة كميكروفون</string>
|
||||
<string name="device_settings_microphone_mode_auto">تلقائي</string>
|
||||
<string name="device_settings_microphone_mode_right">يمين</string>
|
||||
<string name="device_settings_microphone_mode_left">يسار</string>
|
||||
@@ -512,6 +522,10 @@
|
||||
<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="reaction_charged_channel_label">إشعار الشحن الكامل</string>
|
||||
<string name="reaction_charged_notification_title">اكتمل الشحن</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s مشحون بالكامل.</string>
|
||||
<string name="reaction_charged_notification_text_partial">تم شحن %1$s إلى %2$d%%.</string>
|
||||
<string name="device_settings_rename_label">إعادة تسمية</string>
|
||||
<string name="device_settings_rename_hint">اسم الجهاز</string>
|
||||
<string name="device_settings_rename_confirm">إعادة تسمية</string>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Əgər bu yaxınlarda satın almısınızsa, Google Play-in sinxronlaşması bir az vaxt ala bilər.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Alışınız görünmürsə bir neçə dəqiqədən sonra yenidən cəhd edin.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Alış üçün istifadə edilmiş eyni Google hesabı ilə daxil olduğunuzu yoxlayın.</string>
|
||||
<string name="settings_monitor_mode_label">İzləmə rejimi</string>
|
||||
<string name="settings_monitor_mode_description">Bu tətbiqin Bluetooth məlumatlarını hansı şəraitdə izlədiyi.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Əlavə bildiriş</string>
|
||||
<string name="settings_monitor_connected_notification_description">Cihaz qoşulduqda əlavə bildiriş göstərir. Bu, \"Cihaz statusu\" kanalını deaktiv etməklə daimi \"Cihaz yoxdur\" bildirişini gizlətməyə imkan verir.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Ayrıldıqdan sonra bildirişi saxla</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Avtomatik fasilə</string>
|
||||
<string name="settings_autopause_description">Cihazı qulağınızdan çıxardıqda səsi dayandırın.</string>
|
||||
<string name="settings_autopplay_label">Avtomatik oxutma</string>
|
||||
<string name="settings_autoplay_description">Cihaz taxıldıqda səs oxutmasını başladın.</string>
|
||||
<string name="settings_autoplay_description">Podlarınızı yenidən taxdıqda musiqini davam etdirin, lakin yalnız Avtomatik Fasilə onu dayandırdıqda. Özünüz dayandırdığınız fasilələr (telefon, AirPods sapı, yuxu) fasilə vəziyyətində qalır.</string>
|
||||
<string name="settings_start_music_on_wear_label">Taxdıqda musiqini başlat</string>
|
||||
<string name="settings_start_music_on_wear_description">Podlarınızı taxdıqda həmişə musiqi çalmağa çalışır, hətta əl ilə fasilə verdikdən sonra belə.</string>
|
||||
<string name="settings_eardetection_info_label">Qulaq aşkarlama qeydi</string>
|
||||
<string name="settings_eardetection_info_description">Qulaq aşkarlama yalnız bir qulaqlıq üçün işləyirsə, bu Apple məhdudiyyətidir. Yalnız \"əsas qulaqlıq\" (mikrofon üçün istifadə olunan) aşkarlanır. Apple cihazlarında tənzimləyin: Parametrlər → Bluetooth → AirPods → Mikrofon.</string>
|
||||
<string name="settings_fake_data_label">Saxta məlumat</string>
|
||||
<string name="settings_fake_data_description">Saxta məlumat göstər, yəni mövcud olmayan cihazları simulyasiya et.</string>
|
||||
<string name="settings_debug_label">Sazlama parametrləri</string>
|
||||
<string name="settings_debug_description">Tətbiqlə bağlı problemlərin aradan qaldırılmasına kömək edən əlavə parametrlər.</string>
|
||||
<string name="settings_signal_minimum_label">Minimum siqnal keyfiyyəti</string>
|
||||
<string name="settings_signal_minimum_description">Bir cihazın sizə aid hesab edilməsi üçün malik olmalı olduğu minimum siqnal keyfiyyəti.</string>
|
||||
<string name="settings_autoconnect_label">Avtomatik qoşulma</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Qoşulma pop-up-ını göstər</string>
|
||||
<string name="settings_popup_connected_description">Cihaz ilk dəfə qoşulduqda pop-up göstər.</string>
|
||||
<string name="settings_popup_info_not_in_app">Açılan pəncərələr yalnız tətbiqdə olmadığınız zaman göstərilir.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Açılan pəncərələr monitorun işləməsini tələb edir. Monitor rejiminizdə \"Tətbiq açıq olduqda\" seçilmişdir — bu, tətbiqdən çıxdığınız zaman monitoru dayandırır.</string>
|
||||
<string name="general_fix_it_action">Düzəlt</string>
|
||||
<string name="settings_charged_notification_label">Şarj bildirişi</string>
|
||||
<string name="settings_charged_notification_description">Şarj olunan hər şey hədəf şarj səviyyəsinə çatdıqda bildiriş göndərin.</string>
|
||||
<string name="settings_charged_threshold_label">Hədəf şarj səviyyəsi</string>
|
||||
<string name="settings_charged_scope_label">İzlənilən akkumulyatorlar</string>
|
||||
<string name="settings_charged_scope_pods_label">Yalnız qulaqcıqlar</string>
|
||||
<string name="settings_charged_scope_case_label">Yalnız qutu</string>
|
||||
<string name="settings_charged_scope_both_label">Qulaqcıqlar və qutu</string>
|
||||
<string name="device_settings_experimental_title">Eksperimental funksiya</string>
|
||||
<string name="device_settings_experimental_description">Bu funksiya hələ bütün cihazlarda sınaqdan keçirilməyib. Bir şey düzgün işləmirsə, debug loqu ilə problem bildirin.</string>
|
||||
<string name="device_settings_experimental_action">Problem İzləyicisini Aç</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Sazlama</string>
|
||||
<string name="settings_general_label">Parametrlər</string>
|
||||
<string name="settings_general_description">Bütün tətbiqə təsir edən ümumi düzəlişlər.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Uyğun olmayan cihazları gizlət</string>
|
||||
<string name="settings_overview_hide_unmatched_description">İcmalda profillərinizə uyğun gəlməyən yaxın cihazları göstərməyin, məs. başqalarının AirPods-ları.</string>
|
||||
<string name="settings_acknowledgements_label">Təşəkkürlər</string>
|
||||
<string name="settings_debug_autoreports_label">Avtomatik səhv hesabatları</string>
|
||||
<string name="settings_debug_autoreports_description">Problemləri avtomatik olaraq bildirir, məsələn, tətbiqin çökməsi ilə bağlı təfərrüatları, beləliklə onu necə düzəldəcəyimi anlaya bilim.</string>
|
||||
<string name="settings_debug_mode_label">Sazlama rejimi</string>
|
||||
<string name="settings_debug_mode_description">Problemlərin aradan qaldırılması üçün əlavə məlumat göstərin.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Filtrlənməmiş BLE məlumatları</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Bütün yayımlanan BLE məlumatlarını göstərmək üçün BLE skanerindən hər hansı bir filtri çıxarın. Yeni qulaqlıq növləri üçün dəstək əlavə etmək üçün faydalıdır.</string>
|
||||
<string name="settings_compatibility_mode_label">Uyğunluq rejimi</string>
|
||||
<string name="settings_compatibility_mode_description">Uyğunluğu yaxşılaşdırmaq üçün optimallaşdırmaları deaktiv edin. Heç bir məlumat görmürsünüzsə, bunu sınayın.</string>
|
||||
<string name="help_translate_label">Tərcümə</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Bu vidcet cihazınızla birbaşa bağlantı (AAP) tələb edir. Bu vidceti istifadə etməzdən əvvəl cihazınızın tətbiqdə cütləşdirilmiş və qoşulu olduğuna əmin olun.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Bu cihaz səs-küy idarəetməsini dəstəkləmir</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Qoşulmayıb</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Yaxındadır, lakin qoşulmayıb</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Yaxında deyil</string>
|
||||
<string name="anc_widget_aap_connecting_label">Qoşulur…</string>
|
||||
<string name="tile_anc_label">Səs-küy Nəzarəti</string>
|
||||
<string name="tile_anc_subtitle_permission_required">İcazə tələb olunur</string>
|
||||
<string name="tile_anc_subtitle_no_device">Cihaz yoxdur</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Səs-küy nəzarəti yoxdur</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth söndürülüb</string>
|
||||
<string name="widget_config_screen_title">Vidcet Konfiqurasiyası</string>
|
||||
<string name="widget_configuration_title">Cihaz seçin</string>
|
||||
<string name="widget_configuration_description">Bu vidcetin hansı cihaz profilini göstərəcəyini seçin.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth deaktivdir, onu aktivləşdirin ;)</string>
|
||||
<string name="overview_monitoring_active_label">Cihazlar axtarılır</string>
|
||||
<string name="overview_monitoring_active_description">Cihazınızın yaxında və aktiv olduğundan əmin olun.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Qoşuldu, lakin məlumat yoxdur</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Cihazınız qoşulub, lakin CAPod ondan canlı məlumat almır. Telefonunuz uyğunluq seçiminə ehtiyac duya bilər — problem aradan qaldırıcı birini avtomatik tapmağa çalışa bilər.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Problem aradan qaldırıcını işə sal</string>
|
||||
<string name="overview_unmatched_devices_label">Uyğunlaşdırılmamış cihazlar</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">Uyğun profili olmayan %d cihaz</item>
|
||||
@@ -220,16 +229,13 @@
|
||||
<string name="permission_required_title">Aşağıdakı icazə tələb olunur:</string>
|
||||
<string name="permission_system_alert_window_label">Sistem xəbərdarlıq pəncərəsi</string>
|
||||
<string name="permission_system_alert_window_description">\"Pop-up göstər\" xüsusiyyətini mümkün etmək üçün CAPod-a digər tətbiqlərin üzərində çəkməyə icazə verin.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Tətbiq açıq olduqda</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Cihaz qoşulduqda</string>
|
||||
<string name="settings_monitor_mode_always_label">Həmişə</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Görüldüyündə</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Qutu açıqdır</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Qulaqda</string>
|
||||
<string name="pods_dual_left_label">Sol 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>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Şəffaflıq</string>
|
||||
<string name="anc_mode_adaptive">Adaptiv</string>
|
||||
<string name="conversation_awareness_label">Söhbət Məlumatlılığı</string>
|
||||
<string name="settings_conversation_action_label">Danışmağa başladıqda</string>
|
||||
<string name="settings_conversation_action_nothing_label">Heç nə etmə</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Səsi azalt</string>
|
||||
<string name="settings_conversation_action_pause_label">Medianı dayandır</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Səs azaldılması</string>
|
||||
<string name="signal_badge_ble_cd">BLE skanı aktiv</string>
|
||||
<string name="signal_badge_key_irk_cd">Kimlik təsdiqləndi</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Şifrəli bağlantı</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Kəhrəba</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Divar kăğizi rənglərini istifadə edir</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitorinq</string>
|
||||
<string name="settings_category_appearance_label">Görünüş</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Saxlanmamış dəyişikliklər</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Ümumi</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_description">Mikrofonun hansı AirPod tərəfindən istifadə edildiyini</string>
|
||||
<string name="device_settings_microphone_mode_description">Mikrofon kimi hansı qulaqlıq istifadə olunur</string>
|
||||
<string name="device_settings_microphone_mode_auto">Avtomatik</string>
|
||||
<string name="device_settings_microphone_mode_right">Sağ</string>
|
||||
<string name="device_settings_microphone_mode_left">Sol</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Şarj Bildirişi</string>
|
||||
<string name="reaction_charged_notification_title">Şarj tamamlandı</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s tam şarj olundu.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s %2$d%% şarj olundu.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Калі вы нядаўна зрабілі пакупку, Google Play можа спатрэбіцца некаторы час для сінхранізацыі.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Паспрабуйце яшчэ раз праз некалькі хвілін, калі ваша пакупка не з\'яўляецца.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Пераканайцеся, што вы ўвайшлі ў той самы акаўнт Google, які быў выкарыстаны для пакупкі.</string>
|
||||
<string name="settings_monitor_mode_label">Рэжым маніторынгу</string>
|
||||
<string name="settings_monitor_mode_description">Пры якіх абставінах гэта праграма сочыць за данымі Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Дадатковае апавяшчэнне</string>
|
||||
<string name="settings_monitor_connected_notification_description">Паказвае дадатковае апавяшчэнне пры падключэнні прылады. Гэта дазваляе схаваць пастаяннае апавяшчэнне \"Няма прылад\" шляхам адключэння канала \"Стан прылады\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Захаваць апавяшчэнне пасля адключэння</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Аўтаматычная паўза</string>
|
||||
<string name="settings_autopause_description">Прыпыняць аўдыя пры выманні прылады з вуха.</string>
|
||||
<string name="settings_autopplay_label">Аўтаматычнае прайграванне</string>
|
||||
<string name="settings_autoplay_description">Пачынаць прайграванне аўдыя пры надзяванні прылады.</string>
|
||||
<string name="settings_autoplay_description">Аднавіць музыку пры паўторным надзяванні навушнікаў, але толькі калі яе спыніла аўтапаўза. Паўзы, якія вы самі ўключылі (тэлефон, ножка AirPods, сон), застаюцца.</string>
|
||||
<string name="settings_start_music_on_wear_label">Пачынаць музыку пры надзяванні</string>
|
||||
<string name="settings_start_music_on_wear_description">Заўсёды спрабуе прайграць музыку, калі вы надзяваеце навушнікі, нават пасля ручной паўзы.</string>
|
||||
<string name="settings_eardetection_info_label">Заўвага пра вызначэнне вуха</string>
|
||||
<string name="settings_eardetection_info_description">Калі вызначэнне вуха працуе толькі для аднаго навушніка, гэта абмежаванне Apple. Вызначаецца толькі \"асноўны навушнік\" (выкарыстоўваецца для мікрафона). Наладзьце на прыладах Apple: Налады → Bluetooth → AirPods → Мікрафон.</string>
|
||||
<string name="settings_fake_data_label">Падробленыя даныя</string>
|
||||
<string name="settings_fake_data_description">Паказваць падробленыя даныя, г.зн. імітаваць неіснуючыя прылады.</string>
|
||||
<string name="settings_debug_label">Налады адладкі</string>
|
||||
<string name="settings_debug_description">Дадатковыя налады для ліквідацыі непаладак з праграмай.</string>
|
||||
<string name="settings_signal_minimum_label">Мінімальная якасць сігналу</string>
|
||||
<string name="settings_signal_minimum_description">Мінімальная якасць сігналу, якую павінна мець прылада, каб лічыцца вашай.</string>
|
||||
<string name="settings_autoconnect_label">Аўтаматычнае падключэнне</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Паказаць усплывальнае акно падключэння</string>
|
||||
<string name="settings_popup_connected_description">Паказваць усплывальнае акно пры першым падключэнні прылады.</string>
|
||||
<string name="settings_popup_info_not_in_app">Усплывальныя вокны паказваюцца толькі тады, калі вы не знаходзіцеся ў прыкладанні.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Усплывальныя вокны патрабуюць працы манітора. Рэжым манітора ўсталяваны на «Калі прыкладанне адкрыта», які спыняе маніторынг пры выхадзе з прыкладання.</string>
|
||||
<string name="general_fix_it_action">Выправіць</string>
|
||||
<string name="settings_charged_notification_label">Апавяшчэнне аб зарадцы</string>
|
||||
<string name="settings_charged_notification_description">Паказваць апавяшчэнне, калі ўсё, што зараджаецца, дасягне мэтавага ўзроўню зарадкі.</string>
|
||||
<string name="settings_charged_threshold_label">Мэтавы ўзровень зарадкі</string>
|
||||
<string name="settings_charged_scope_label">Адсочваемыя батарэі</string>
|
||||
<string name="settings_charged_scope_pods_label">Толькі навушнікі</string>
|
||||
<string name="settings_charged_scope_case_label">Толькі футарал</string>
|
||||
<string name="settings_charged_scope_both_label">Навушнікі і футарал</string>
|
||||
<string name="device_settings_experimental_title">Эксперыментальная функцыя</string>
|
||||
<string name="device_settings_experimental_description">Гэта функцыя яшчэ не была праверана на ўсіх прыладах. Калі нешта не працуе правільна, калі ласка, стварыце запыт з журналам адладкі.</string>
|
||||
<string name="device_settings_experimental_action">Адкрыць трэкер задач</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Адладка</string>
|
||||
<string name="settings_general_label">Налады</string>
|
||||
<string name="settings_general_description">Агульныя налады, якія ўплываюць на ўсю праграму.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Хаваць непрызнаныя прылады</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Не паказваць у агляднічку блізкія прылады, якія не адпавядаюць ніводнаму з вашых профіляў, напр. AirPods іншых людзей.</string>
|
||||
<string name="settings_acknowledgements_label">Падзякі</string>
|
||||
<string name="settings_debug_autoreports_label">Аўтаматычныя справаздачы пра памылкі</string>
|
||||
<string name="settings_debug_autoreports_description">Аўтаматычна паведамляе пра праблемы, напрыклад, звесткі пра збой праграмы, каб я мог зразумець, як гэта выправіць.</string>
|
||||
<string name="settings_debug_mode_label">Рэжым адладкі</string>
|
||||
<string name="settings_debug_mode_description">Паказваць дадатковую інфармацыю для ліквідацыі непаладак.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Нефільтраваныя даныя BLE</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Выдаліце ўсе фільтры са сканера BLE, каб паказаць усе трансляваныя даныя BLE. Карысна для дадання падтрымкі новых тыпаў навушнікаў.</string>
|
||||
<string name="settings_compatibility_mode_label">Рэжым сумяшчальнасці</string>
|
||||
<string name="settings_compatibility_mode_description">Адключыце аптымізацыі для паляпшэння сумяшчальнасці. Паспрабуйце гэта, калі вы не бачыце ніякіх даных.</string>
|
||||
<string name="help_translate_label">Пераклад</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Гэты віджэт патрабуе прамога злучэння (AAP) з вашай прыладай. Пераканайцеся, што ваша прылада спалучана і падключана ў праграме перад выкарыстаннем гэтага віджэта.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Гэта прылада не падтрымлівае кіраванне шумам</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Не падключана</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Побач, але не падключана</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Не побач</string>
|
||||
<string name="anc_widget_aap_connecting_label">Падключэнне…</string>
|
||||
<string name="tile_anc_label">Кіраванне шумам</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Патрабуецца дазвол</string>
|
||||
<string name="tile_anc_subtitle_no_device">Няма прылады</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Няма кіравання шумам</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth выключаны</string>
|
||||
<string name="widget_config_screen_title">Канфігурацыя віджэта</string>
|
||||
<string name="widget_configuration_title">Выбраць прыладу</string>
|
||||
<string name="widget_configuration_description">Выберыце, які профіль прылады павінен адлюстроўваць гэты віджэт.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth адключаны, уключыце яго ;)</string>
|
||||
<string name="overview_monitoring_active_label">Маніторынг прылад</string>
|
||||
<string name="overview_monitoring_active_description">Пераканайцеся, што ваша прылада побач і актыўная.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Падлучана, але дадзеных няма</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Ваша прылада падлучана, але CAPod не атрымлівае ніякіх жывых дадзеных з яе. Вашаму тэлефону можа спатрэбіцца параметр сумяшчальнасці — ліквідатар непаладак можа паспрабаваць знайсці яго аўтаматычна.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Запусціць ліквідатар непаладак</string>
|
||||
<string name="overview_unmatched_devices_label">Неадпаведныя прылады</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d прылада без адпаведнага профілю</item>
|
||||
@@ -224,9 +233,6 @@
|
||||
<string name="permission_required_title">Патрабуецца наступны дазвол:</string>
|
||||
<string name="permission_system_alert_window_label">Сістэмнае акно папярэджання</string>
|
||||
<string name="permission_system_alert_window_description">Дазвольце CAPod маляваць паверх іншых праграм, каб зрабіць магчымай функцыю \"Паказаць усплывальнае акно\".</string>
|
||||
<string name="settings_monitor_mode_manual_label">Калі праграма адкрыта</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Калі прылада падключана</string>
|
||||
<string name="settings_monitor_mode_always_label">Заўсёды</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Калі бачна</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Корпус адкрыты</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">У вуху</string>
|
||||
@@ -258,6 +264,11 @@
|
||||
<string name="anc_mode_transparency">Празрыстасць</string>
|
||||
<string name="anc_mode_adaptive">Адаптыўны</string>
|
||||
<string name="conversation_awareness_label">Усведамленне размовы</string>
|
||||
<string name="settings_conversation_action_label">Калі вы пачынаеце гаварыць</string>
|
||||
<string name="settings_conversation_action_nothing_label">Нічога не рабіць</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Зменшыць гучнасць</string>
|
||||
<string name="settings_conversation_action_pause_label">Паставіць медыя на паўзу</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Зніжэнне гучнасці</string>
|
||||
<string name="signal_badge_ble_cd">BLE-сканаванне актыўнае</string>
|
||||
<string name="signal_badge_key_irk_cd">Асоба пацверджана</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Зашыфраванае злучэнне</string>
|
||||
@@ -326,7 +337,6 @@
|
||||
<string name="ui_theme_color_amber_label">Бурштынавы</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Выкарыстоўвае колеры шпалеры</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Маніторынг</string>
|
||||
<string name="settings_category_appearance_label">Знешні выгляд</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Незахаваныя змены</string>
|
||||
@@ -480,7 +490,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Агульныя</string>
|
||||
<string name="device_settings_microphone_mode_label">Мікрафон</string>
|
||||
<string name="device_settings_microphone_mode_description">Які AirPod выкарыстоўваецца як мікрафон</string>
|
||||
<string name="device_settings_microphone_mode_description">Які навушнік выкарыстоўваецца як мікрафон</string>
|
||||
<string name="device_settings_microphone_mode_auto">Аўта</string>
|
||||
<string name="device_settings_microphone_mode_right">Правы</string>
|
||||
<string name="device_settings_microphone_mode_left">Левы</string>
|
||||
@@ -502,6 +512,10 @@
|
||||
<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="reaction_charged_channel_label">Апавяшчэнне аб зарадцы</string>
|
||||
<string name="reaction_charged_notification_title">Зарадка завершана</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s поўнасцю зараджаны.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s зараджаны да %2$d%%.</string>
|
||||
<string name="device_settings_rename_label">Перайменаваць</string>
|
||||
<string name="device_settings_rename_hint">Назва прылады</string>
|
||||
<string name="device_settings_rename_confirm">Перайменаваць</string>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Ако наскоро сте направили покупка, може да отнеме малко, докато Google Play синхронизира.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Опитайте отново след няколко минути, ако покупката ви не се появи.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Уверете се, че сте влезли с акаунта в Google, използван за покупката.</string>
|
||||
<string name="settings_monitor_mode_label">Режим на наблюдение</string>
|
||||
<string name="settings_monitor_mode_description">При какви обстоятелства това приложение следи Bluetooth данните.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Допълнително известие</string>
|
||||
<string name="settings_monitor_connected_notification_description">Показва допълнително известие, когато устройство е свързано. Това ви позволява да скриете постоянното известие \"Няма устройства\" чрез деактивиране на канала \"Състояние на устройството\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Запази известието след прекъсване</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Автоматична пауза</string>
|
||||
<string name="settings_autopause_description">Поставяне на аудио на пауза при премахване на устройството от ухото ви.</string>
|
||||
<string name="settings_autopplay_label">Автоматично възпроизвеждане</string>
|
||||
<string name="settings_autoplay_description">Стартиране на аудио възпроизвеждане, когато устройството се носи.</string>
|
||||
<string name="settings_autoplay_description">Възобновява музиката, когато отново сложите слушалките, но само ако е спряна от Автоматично пауза. Паузите, направени от вас (телефон, дръжка на AirPods, заспиване), остават.</string>
|
||||
<string name="settings_start_music_on_wear_label">Стартиране на музика при поставяне</string>
|
||||
<string name="settings_start_music_on_wear_description">Винаги се опитва да пусне музика, когато сложите слушалките, дори след ръчна пауза.</string>
|
||||
<string name="settings_eardetection_info_label">Бележка за разпознаване на ухото</string>
|
||||
<string name="settings_eardetection_info_description">Ако разпознаването на ухото работи само за една слушалка, това е ограничение на Apple. Само \"основната слушалка\" (използвана за микрофон) се разпознава. Конфигурирайте на Apple устройства: Настройки → Bluetooth → AirPods → Микрофон.</string>
|
||||
<string name="settings_fake_data_label">Фалшиви данни</string>
|
||||
<string name="settings_fake_data_description">Показване на фалшиви данни, т.е. симулиране на устройства, които не съществуват.</string>
|
||||
<string name="settings_debug_label">Настройки за отстраняване на грешки</string>
|
||||
<string name="settings_debug_description">Допълнителни настройки за подпомагане на отстраняването на проблеми с приложението.</string>
|
||||
<string name="settings_signal_minimum_label">Минимално качество на сигнала</string>
|
||||
<string name="settings_signal_minimum_description">Минималното качество на сигнала, което устройството трябва да има, за да се счита за ваше.</string>
|
||||
<string name="settings_autoconnect_label">Автоматично свързване</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Показване на изскачащ прозорец за връзка</string>
|
||||
<string name="settings_popup_connected_description">Показване на изскачащ прозорец, когато устройството се свърже за първи път.</string>
|
||||
<string name="settings_popup_info_not_in_app">Изскачащите прозорци се показват само когато не сте в приложението.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Изскачащите прозорци изискват мониторът да работи. Режимът на монитора е зададен на „Когато приложението е отворено“, което спира монитора, когато напуснете приложението.</string>
|
||||
<string name="general_fix_it_action">Поправи го</string>
|
||||
<string name="settings_charged_notification_label">Известие за зареждане</string>
|
||||
<string name="settings_charged_notification_description">Показва известие, когато всичко, което се зарежда, достигне желаното ниво на заряд.</string>
|
||||
<string name="settings_charged_threshold_label">Целево ниво на заряд</string>
|
||||
<string name="settings_charged_scope_label">Наблюдавани батерии</string>
|
||||
<string name="settings_charged_scope_pods_label">Само слушалки</string>
|
||||
<string name="settings_charged_scope_case_label">Само калъф</string>
|
||||
<string name="settings_charged_scope_both_label">Слушалки и калъф</string>
|
||||
<string name="device_settings_experimental_title">Експериментална функция</string>
|
||||
<string name="device_settings_experimental_description">Тази функция все още не е тествана на всички устройства. Ако нещо не работи правилно, моля отворете проблем с дневник за отстраняване на грешки.</string>
|
||||
<string name="device_settings_experimental_action">Отвори проследявач на проблеми</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Отстраняване на грешки</string>
|
||||
<string name="settings_general_label">Настройки</string>
|
||||
<string name="settings_general_description">Общи настройки, които засягат цялото приложение.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Скриване на несъответстващи устройства</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Не показвай намиращи се наблизо устройства, които не съответстват на нито един от профилите ти в прегледа, напр. AirPods на други хора.</string>
|
||||
<string name="settings_acknowledgements_label">Благодарности</string>
|
||||
<string name="settings_debug_autoreports_label">Автоматични доклади за грешки</string>
|
||||
<string name="settings_debug_autoreports_description">Автоматично докладва проблеми, напр. подробности за срив на приложението, за да мога да разбера как да го поправя.</string>
|
||||
<string name="settings_debug_mode_label">Режим за отстраняване на грешки</string>
|
||||
<string name="settings_debug_mode_description">Показване на допълнителна информация за отстраняване на проблеми.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Нефилтрирани BLE данни</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Премахнете всички филтри от BLE скенера, за да се покажат всички излъчвани BLE данни. Полезно за добавяне на поддръжка за нови типове слушалки.</string>
|
||||
<string name="settings_compatibility_mode_label">Режим на съвместимост</string>
|
||||
<string name="settings_compatibility_mode_description">Деактивирайте оптимизациите, за да подобрите съвместимостта. Опитайте това, ако не виждате никакви данни.</string>
|
||||
<string name="help_translate_label">Превод</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Този уиджет изисква директна връзка (AAP) с вашето устройство. Уверете се, че устройството е сдвоено и свързано в приложението преди да използвате този уиджет.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Това устройство не поддържа контрол на шума</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Не е свързано</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Наблизо, но не свързано</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Не е наблизо</string>
|
||||
<string name="anc_widget_aap_connecting_label">Свързване…</string>
|
||||
<string name="tile_anc_label">Шумов контрол</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Необходимо е разрешение</string>
|
||||
<string name="tile_anc_subtitle_no_device">Няма устройство</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Без шумов контрол</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth е изключен</string>
|
||||
<string name="widget_config_screen_title">Настройка на джаджата</string>
|
||||
<string name="widget_configuration_title">Избор на устройство</string>
|
||||
<string name="widget_configuration_description">Изберете кой профил на устройство да показва това приспособление.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth е деактивиран, активирайте го ;)</string>
|
||||
<string name="overview_monitoring_active_label">Наблюдение на устройства</string>
|
||||
<string name="overview_monitoring_active_description">Уверете се, че устройството ви е наблизо и активно.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Свързан, но без данни</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Устройството ти е свързано, но CAPod не получава данни от него в реално време. Телефонът ти може да се нуждае от опция за съвместимост — инструментът за отстраняване на неизправности може да се опита да намери такава автоматично.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Стартирай отстраняване на неизправности</string>
|
||||
<string name="overview_unmatched_devices_label">Несъвпадащи устройства</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d устройство без съвпадащ профил</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Необходимо е следното разрешение:</string>
|
||||
<string name="permission_system_alert_window_label">Системен изскачащ прозорец</string>
|
||||
<string name="permission_system_alert_window_description">Позволете на CAPod да рисува върху други приложения, за да направи функцията \"Покажи изскачащ прозорец\" възможна.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Когато приложението е отворено</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Когато устройството е свързано</string>
|
||||
<string name="settings_monitor_mode_always_label">Винаги</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Когато е видяно</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Калъфът е отворен</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">В ухото</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Прозрачност</string>
|
||||
<string name="anc_mode_adaptive">Адаптивен</string>
|
||||
<string name="conversation_awareness_label">Разговорна осъзнатост</string>
|
||||
<string name="settings_conversation_action_label">Когато започнете да говорите</string>
|
||||
<string name="settings_conversation_action_nothing_label">Без действие</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Намаляване на силата на звука</string>
|
||||
<string name="settings_conversation_action_pause_label">Пауза на медията</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Намаляване на силата на звука</string>
|
||||
<string name="signal_badge_ble_cd">BLE сканирането е активно</string>
|
||||
<string name="signal_badge_key_irk_cd">Самоличността е потвърдена</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Шифрована връзка</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Къхъленожълт</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Използва цветовете на тапета</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Мониторинг</string>
|
||||
<string name="settings_category_appearance_label">Изглед</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Незапазени промени</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Общи</string>
|
||||
<string name="device_settings_microphone_mode_label">Микрофон</string>
|
||||
<string name="device_settings_microphone_mode_description">Кой AirPod се използва като микрофон</string>
|
||||
<string name="device_settings_microphone_mode_description">Коя слушалка се използва като микрофон</string>
|
||||
<string name="device_settings_microphone_mode_auto">Автом.</string>
|
||||
<string name="device_settings_microphone_mode_right">Десен</string>
|
||||
<string name="device_settings_microphone_mode_left">Ляв</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Известие за зареждане</string>
|
||||
<string name="reaction_charged_notification_title">Зареждането е завършено</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s е напълно зареден.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s е зареден до %2$d%%.</string>
|
||||
<string name="device_settings_rename_label">Преименуване</string>
|
||||
<string name="device_settings_rename_hint">Наименование на устройството</string>
|
||||
<string name="device_settings_rename_confirm">Преименуване</string>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">আপনি সম্প্রতি কিনে থাকলে, Google Play সিঙ্ক করতে কিছু সময় লাগতে পারে।</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">কয়েক মিনিট পরে আবার চেষ্টা করুন যদি আপনার ক্রয় না দেখায়।</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">নিশ্চিত করুন যে আপনি ক্রয়ের জন্য ব্যবহৃত একই Google অ্যাকাউন্ট দিয়ে সাইন ইন করেছেন।</string>
|
||||
<string name="settings_monitor_mode_label">মনিটর মোড</string>
|
||||
<string name="settings_monitor_mode_description">এই অ্যাপটি কোন পরিস্থিতিতে ব্লুটুথ ডেটা পর্যবেক্ষণ করবে।</string>
|
||||
<string name="settings_monitor_connected_notification_label">অতিরিক্ত বিজ্ঞপ্তি</string>
|
||||
<string name="settings_monitor_connected_notification_description">একটি ডিভাইস সংযুক্ত হলে একটি অতিরিক্ত বিজ্ঞপ্তি দেখায়। এটি আপনাকে \"ডিভাইস স্ট্যাটাস\" চ্যানেল নিষ্ক্রিয় করে স্থায়ী \"কোনো ডিভাইস নেই\" বিজ্ঞপ্তি লুকাতে দেয়।</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">সংযোগ বিচ্ছিন্নের পরে বিজ্ঞপ্তি রাখুন</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">স্বয়ংক্রিয় বিরতি</string>
|
||||
<string name="settings_autopause_description">আপনার কান থেকে ডিভাইসটি সরানোর সময় অডিওতে বিরতি দিন।</string>
|
||||
<string name="settings_autopplay_label">স্বয়ংক্রিয় প্লে</string>
|
||||
<string name="settings_autoplay_description">ডিভাইস পরিধান করার সময় অডিও প্লেব্যাক শুরু করুন।</string>
|
||||
<string name="settings_autoplay_description">আপনার পড পুনরায় পরলে সংগীত চালু করুন, তবে শুধুমাত্র যদি অটো পজ এটি থামিয়ে দিয়ে থাকে। আপনি নিজে যে পজ করেছেন (ফোন, AirPods স্টেম, স্লিপ) সেগুলো পজ থাকবে।</string>
|
||||
<string name="settings_start_music_on_wear_label">পরলে সংগীত শুরু করুন</string>
|
||||
<string name="settings_start_music_on_wear_description">আপনি পড পরলে সবসময় সংগীত চালানোর চেষ্টা করে, এমনকি ম্যানুয়াল পজের পরেও।</string>
|
||||
<string name="settings_eardetection_info_label">কান সনাক্তকরণ নোট</string>
|
||||
<string name="settings_eardetection_info_description">যদি কান সনাক্তকরণ শুধুমাত্র একটি পডের জন্য কাজ করে, এটি একটি Apple সীমাবদ্ধতা। শুধুমাত্র \"প্রাথমিক পড\" (মাইক্রোফোনের জন্য ব্যবহৃত) সনাক্ত করা হয়। Apple ডিভাইসে কনফিগার করুন: সেটিংস → Bluetooth → AirPods → মাইক্রোফোন।</string>
|
||||
<string name="settings_fake_data_label">ভুয়া ডেটা</string>
|
||||
<string name="settings_fake_data_description">ভুয়া ডেটা দেখান, অর্থাৎ, এমন ডিভাইস সিমুলেট করুন যা বিদ্যমান নেই।</string>
|
||||
<string name="settings_debug_label">ডিবাগ সেটিংস</string>
|
||||
<string name="settings_debug_description">অ্যাপের সমস্যা সমাধানে সহায়তার জন্য অতিরিক্ত সেটিংস।</string>
|
||||
<string name="settings_signal_minimum_label">ন্যূনতম সংকেত গুণমান</string>
|
||||
<string name="settings_signal_minimum_description">একটি ডিভাইসকে আপনার হিসাবে বিবেচনা করার জন্য ন্যূনতম সংকেত গুণমান থাকতে হবে।</string>
|
||||
<string name="settings_autoconnect_label">স্বয়ংক্রিয় সংযোগ</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">সংযোগ পপআপ দেখান</string>
|
||||
<string name="settings_popup_connected_description">ডিভাইস প্রথমবার সংযুক্ত হলে একটি পপআপ দেখান।</string>
|
||||
<string name="settings_popup_info_not_in_app">পপআপগুলি শুধুমাত্র তখনই দেখানো হয় যখন আপনি অ্যাপে থাকেন না।</string>
|
||||
<string name="settings_popup_warning_manual_mode">পপআপের জন্য মনিটর চালু থাকা প্রয়োজন। আপনার মনিটর মোড \"যখন অ্যাপ খোলা থাকে\" তে সেট করা আছে, যা আপনি অ্যাপ ছেড়ে গেলে মনিটর বন্ধ করে দেয়।</string>
|
||||
<string name="general_fix_it_action">ঠিক করুন</string>
|
||||
<string name="settings_charged_notification_label">চার্জ সম্পন্ন বিজ্ঞপ্তি</string>
|
||||
<string name="settings_charged_notification_description">চার্জ হচ্ছে এমন সবকিছু লক্ষ্যমাত্রা চার্জ স্তরে পৌঁছালে একটি বিজ্ঞপ্তি দেখান।</string>
|
||||
<string name="settings_charged_threshold_label">লক্ষ্যমাত্রা চার্জ স্তর</string>
|
||||
<string name="settings_charged_scope_label">পর্যবেক্ষণ করা ব্যাটারি</string>
|
||||
<string name="settings_charged_scope_pods_label">শুধুমাত্র পড</string>
|
||||
<string name="settings_charged_scope_case_label">শুধুমাত্র কেস</string>
|
||||
<string name="settings_charged_scope_both_label">পড এবং কেস</string>
|
||||
<string name="device_settings_experimental_title">পরীক্ষামূলক ফিচার</string>
|
||||
<string name="device_settings_experimental_description">এই ফিচারটি এখনো সব ডিভাইসে পরীক্ষা করা হয়নি। যদি কিছু সঠিকভাবে কাজ না করে, দয়া করে একটি ডিবাগ লগ সহ একটি ইস্যু খুলুন।</string>
|
||||
<string name="device_settings_experimental_action">ইস্যু ট্র্যাকার খুলুন</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">ডিবাগ</string>
|
||||
<string name="settings_general_label">সেটিংস</string>
|
||||
<string name="settings_general_description">পুরো অ্যাপকে প্রভাবিত করে এমন সাধারণ পরিবর্তন।</string>
|
||||
<string name="settings_overview_hide_unmatched_label">অমিল ডিভাইস লুকান</string>
|
||||
<string name="settings_overview_hide_unmatched_description">ওভারভিউতে কাছের এমন ডিভাইস দেখাবে না যেগুলো আপনার কোনো প্রোফাইলের সাথে মেলে না, যেমন অন্যদের AirPods।</string>
|
||||
<string name="settings_acknowledgements_label">স্বীকৃতি</string>
|
||||
<string name="settings_debug_autoreports_label">স্বয়ংক্রিয় বাগ রিপোর্ট</string>
|
||||
<string name="settings_debug_autoreports_description">স্বয়ংক্রিয়ভাবে সমস্যাগুলি রিপোর্ট করে, যেমন অ্যাপ ক্র্যাশের বিবরণ যাতে আমি এটি কীভাবে ঠিক করতে পারি তা বের করতে পারি।</string>
|
||||
<string name="settings_debug_mode_label">ডিবাগ মোড</string>
|
||||
<string name="settings_debug_mode_description">সমস্যা সমাধানের জন্য অতিরিক্ত তথ্য দেখান।</string>
|
||||
<string name="settings_blescanner_unfiltered_label">ফিল্টারবিহীন BLE ডেটা</string>
|
||||
<string name="settings_blescanner_unfiltered_description">সমস্ত সম্প্রচারিত BLE ডেটা দেখানোর জন্য BLE স্ক্যানার থেকে যেকোনো ফিল্টার সরান। নতুন হেডফোন প্রকারের জন্য সমর্থন যোগ করার জন্য দরকারী।</string>
|
||||
<string name="settings_compatibility_mode_label">সামঞ্জস্যতা মোড</string>
|
||||
<string name="settings_compatibility_mode_description">সামঞ্জস্যতা উন্নত করতে অপ্টিমাইজেশান নিষ্ক্রিয় করুন। যদি আপনি কোনো ডেটা দেখতে না পান তবে এটি চেষ্টা করুন।</string>
|
||||
<string name="help_translate_label">অনুবাদ</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">এই উইজেটটি আপনার ডিভাইসে একটি সরাসরি সংযোগ (AAP) প্রয়োজন। এই উইজেটটি ব্যবহার করার আগে নিশ্চিত করুন যে আপনার ডিভাইসটি অ্যাপে পেয়ার এবং সংযুক্ত আছে।</string>
|
||||
<string name="anc_widget_no_anc_support_label">এই ডিভাইসটি নয়েজ কন্ট্রোল সমর্থন করে না</string>
|
||||
<string name="anc_widget_aap_not_connected_label">সংযুক্ত নয়</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">কাছাকাছি কিন্তু সংযুক্ত নয়</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">কাছাকাছি নয়</string>
|
||||
<string name="anc_widget_aap_connecting_label">সংযুক্ত হচ্ছে…</string>
|
||||
<string name="tile_anc_label">নয়েজ কন্ট্রোল</string>
|
||||
<string name="tile_anc_subtitle_permission_required">অনুমতি প্রয়োজন</string>
|
||||
<string name="tile_anc_subtitle_no_device">কোনও ডিভাইস নেই</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">নয়েজ কন্ট্রোল নেই</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">ব্লুটুথ বন্ধ</string>
|
||||
<string name="widget_config_screen_title">উইজেট কনফিগারেশন</string>
|
||||
<string name="widget_configuration_title">ডিভাইস নির্বাচন করুন</string>
|
||||
<string name="widget_configuration_description">এই উইজেটটি কোন ডিভাইস প্রোফাইল প্রদর্শন করবে তা চয়ন করুন।</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth নিষ্ক্রিয়, এটি সক্রিয় করুন ;)</string>
|
||||
<string name="overview_monitoring_active_label">ডিভাইসের জন্য পর্যবেক্ষণ চলছে</string>
|
||||
<string name="overview_monitoring_active_description">নিশ্চিত করুন যে আপনার ডিভাইস কাছে আছে এবং সক্রিয়।</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">সংযুক্ত, কিন্তু কোনো ডেটা নেই</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">আপনার ডিভাইসটি সংযুক্ত, কিন্তু CAPod এটি থেকে কোনো লাইভ ডেটা পাচ্ছে না। আপনার ফোনের একটি সামঞ্জস্যতা বিকল্পের প্রয়োজন হতে পারে — সমস্যা সমাধানকারী স্বয়ংক্রিয়ভাবে একটি খুঁজে পেতে চেষ্টা করতে পারে।</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">সমস্যা সমাধানকারী চালান</string>
|
||||
<string name="overview_unmatched_devices_label">অমিলিত ডিভাইস</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">মিলিত প্রোফাইল ছাড়া %d ডিভাইস</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">নিম্নলিখিত অনুমতি প্রয়োজন:</string>
|
||||
<string name="permission_system_alert_window_label">সিস্টেম অ্যালার্ট উইন্ডো</string>
|
||||
<string name="permission_system_alert_window_description">\"পপআপ দেখান\" বৈশিষ্ট্যটি সম্ভব করতে CAPod-কে অন্যান্য অ্যাপের উপরে আঁকতে অনুমতি দিন।</string>
|
||||
<string name="settings_monitor_mode_manual_label">অ্যাপ খোলা থাকলে</string>
|
||||
<string name="settings_monitor_mode_automatic_label">ডিভাইস সংযুক্ত থাকলে</string>
|
||||
<string name="settings_monitor_mode_always_label">সর্বদা</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">দেখা গেলে</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">কেস খোলা</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">কানে</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">ট্রান্সপারেন্সি</string>
|
||||
<string name="anc_mode_adaptive">অ্যাডাপটিভ</string>
|
||||
<string name="conversation_awareness_label">কথোপকথন সচেতনতা</string>
|
||||
<string name="settings_conversation_action_label">যখন আপনি কথা বলতে শুরু করেন</string>
|
||||
<string name="settings_conversation_action_nothing_label">কিছু করবেন না</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">ভলিউম কমান</string>
|
||||
<string name="settings_conversation_action_pause_label">মিডিয়া পজ করুন</string>
|
||||
<string name="settings_conversation_volume_reduction_label">ভলিউম হ্রাস</string>
|
||||
<string name="signal_badge_ble_cd">BLE স্ক্যান সক্রিয়</string>
|
||||
<string name="signal_badge_key_irk_cd">পরিচয় যাচাই করা হয়েছে</string>
|
||||
<string name="signal_badge_key_encrypted_cd">এনক্রিপ্টেড সংযোগ</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">অ্যাম্বার</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">ওয়ালপেপার রঙ ব্যবহার করে</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">মনিটরিং</string>
|
||||
<string name="settings_category_appearance_label">উপস্থিতি</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">অসংরক্ষিত পরিবর্তন</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">সাধারণ</string>
|
||||
<string name="device_settings_microphone_mode_label">মাইক্রোফোন</string>
|
||||
<string name="device_settings_microphone_mode_description">কোন AirPod মাইক্রোফোন হিসেবে ব্যবহৃত হয়</string>
|
||||
<string name="device_settings_microphone_mode_description">কোন ইয়ারবাড মাইক্রোফোন হিসেবে ব্যবহার করা হয়</string>
|
||||
<string name="device_settings_microphone_mode_auto">স্বয়ংক্রিয়</string>
|
||||
<string name="device_settings_microphone_mode_right">ডান</string>
|
||||
<string name="device_settings_microphone_mode_left">বাঁ</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">চার্জ সম্পন্ন বিজ্ঞপ্তি</string>
|
||||
<string name="reaction_charged_notification_title">চার্জিং সম্পন্ন</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s সম্পূর্ণ চার্জ হয়েছে।</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s %2$d%% পর্যন্ত চার্জ হয়েছে।</string>
|
||||
<string name="device_settings_rename_label">নাম বদলান</string>
|
||||
<string name="device_settings_rename_hint">ডিভাইসের নাম</string>
|
||||
<string name="device_settings_rename_confirm">নাম বদলান</string>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Si heu comprat alguna cosa recentment, és possible que el Google Play trigui una estona a sincronitzar-se.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Torneu-ho a provar en uns minuts si la vostra compra no apareix.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Assegureu-vos d\'haver iniciat la sessió amb el mateix compte de Google que heu utilitzat per a la compra.</string>
|
||||
<string name="settings_monitor_mode_label">Mode monitor</string>
|
||||
<string name="settings_monitor_mode_description">En quines circumstàncies aquesta aplicació supervisa les dades Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Notificació addicional</string>
|
||||
<string name="settings_monitor_connected_notification_description">Mostra una notificació addicional quan hi ha un dispositiu connectat. Això permet ocultar la notificació permanent «Sense dispositius» desactivant el canal «Estat del dispositiu».</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Mantén les notificacions després de la desconnexió</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Pausa automàtica</string>
|
||||
<string name="settings_autopause_description">Pausa l\'àudio quan ús tragueu el dispositiu de l\'orella.</string>
|
||||
<string name="settings_autopplay_label">Reproducció automàtica</string>
|
||||
<string name="settings_autoplay_description">Inicia la reproducció d\'àudio en utilitzar el dispositiu.</string>
|
||||
<string name="settings_autoplay_description">Reprèn la música quan torneu a posar-vos els auriculars, però només si la pausa automàtica l\'havia aturat. Les pauses que heu activat vos mateix (telèfon, pliques dels AirPods, son) romanen en pausa.</string>
|
||||
<string name="settings_start_music_on_wear_label">Inicia la música en posar-vos els auriculars</string>
|
||||
<string name="settings_start_music_on_wear_description">Sempre intenta reproduir música quan us poseu els auriculars, fins i tot després d\'una pausa manual.</string>
|
||||
<string name="settings_eardetection_info_label">Nota de detecció de l\'oïda</string>
|
||||
<string name="settings_eardetection_info_description">Si la detecció de l\'oïda només funciona per a un auricular, això és degut a una limitació d\'Apple. Només es detecta «l\'auricular principal» (utilitzat per al micròfon). Configureu-ho en dispositius Apple: Configuració → Bluetooth → AirPods → Micròfon.</string>
|
||||
<string name="settings_fake_data_label">Dades falses</string>
|
||||
<string name="settings_fake_data_description">Mostra dades falses. Per exemple: simula dispositius que no existeixen.</string>
|
||||
<string name="settings_debug_label">Configuració de depuració</string>
|
||||
<string name="settings_debug_description">Configuració addicional per ajudar a resoldre problemes amb l\'aplicació.</string>
|
||||
<string name="settings_signal_minimum_label">Qualitat de senyal mínima</string>
|
||||
<string name="settings_signal_minimum_description">La qualitat de senyal mínima que ha de tenir un dispositiu per considerar-se vostre.</string>
|
||||
<string name="settings_autoconnect_label">Connexió automàtica</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Mostra la finestra emergent de connexió</string>
|
||||
<string name="settings_popup_connected_description">Mostra una finestra emergent quan el dispositiu es connecti per primera vegada.</string>
|
||||
<string name="settings_popup_info_not_in_app">Les finestres emergents només es mostren quan no esteu a l\'aplicació.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Les finestres emergents requereixen que el monitor estigui en execució. El mode del monitor està configurat com a «quan l\'aplicació és oberta», que atura el monitor quan sortiu de l\'aplicació.</string>
|
||||
<string name="general_fix_it_action">Corregeix-ho</string>
|
||||
<string name="settings_charged_notification_label">Notificació de càrrega completa</string>
|
||||
<string name="settings_charged_notification_description">Mostra una notificació quan tots els dispositius que s\'estiguin carregant arribin al nivell de càrrega desitjat.</string>
|
||||
<string name="settings_charged_threshold_label">Nivell de càrrega objectiu</string>
|
||||
<string name="settings_charged_scope_label">Bateries monitoritzades</string>
|
||||
<string name="settings_charged_scope_pods_label">Només els auriculars</string>
|
||||
<string name="settings_charged_scope_case_label">Només la funda</string>
|
||||
<string name="settings_charged_scope_both_label">Auriculars i funda</string>
|
||||
<string name="device_settings_experimental_title">Funció experimental</string>
|
||||
<string name="device_settings_experimental_description">Aquesta funció encara no s\'ha provat a tots els dispositius. Si alguna cosa no funciona correctament, obriu una incidència amb un registre de depuració.</string>
|
||||
<string name="device_settings_experimental_action">Obre el seguidor d\'incidències</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Depuració</string>
|
||||
<string name="settings_general_label">Configuració</string>
|
||||
<string name="settings_general_description">Configuracions generals que afecten a tota l\'aplicació.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Amaga els dispositius no coincidents</string>
|
||||
<string name="settings_overview_hide_unmatched_description">No mostris els dispositius propers que no coincideixen amb cap dels perfils a la vista general. P. ex. els AirPods d’altres persones.</string>
|
||||
<string name="settings_acknowledgements_label">Agraïments</string>
|
||||
<string name="settings_debug_autoreports_label">Informes automàtics d\'errors</string>
|
||||
<string name="settings_debug_autoreports_description">Informa automàticament de problemes. Per exemple: detalls sobre un bloqueig de l\'aplicació perquè pugui esbrinar com solucionar-ho.</string>
|
||||
<string name="settings_debug_mode_label">Mode depuració</string>
|
||||
<string name="settings_debug_mode_description">Mostra informació addicional per resoldre problemes.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Dades BLE sense filtrar</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Elimina els filtres de l\'escàner BLE per mostrar totes les dades BLE emeses. Útil per afegir suport a nous tipus d\'auriculars.</string>
|
||||
<string name="settings_compatibility_mode_label">Mode de compatibilitat</string>
|
||||
<string name="settings_compatibility_mode_description">Desactiva les optimitzacions per millorar la compatibilitat. Proveu això si no veieu cap dada.</string>
|
||||
<string name="help_translate_label">Traducció</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Aquest giny requereix una connexió directa (AAP) al dispositiu. Assegureu-vos que el dispositiu estigui emparellat i connectat a l\'aplicació abans d\'utilitzar aquest giny.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Aquest dispositiu no admet el control de soroll</string>
|
||||
<string name="anc_widget_aap_not_connected_label">No connectat</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">A prop però no connectat</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">No és a prop</string>
|
||||
<string name="anc_widget_aap_connecting_label">S\'està connectant…</string>
|
||||
<string name="tile_anc_label">Control de soroll</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Permís requerit</string>
|
||||
<string name="tile_anc_subtitle_no_device">Cap dispositiu</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Sense control de soroll</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth desactivat</string>
|
||||
<string name="widget_config_screen_title">Configuració del giny</string>
|
||||
<string name="widget_configuration_title">Seleccioneu un dispositiu</string>
|
||||
<string name="widget_configuration_description">Trieu quin perfil de dispositiu ha de mostrar aquest giny.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">El Bluetooth està desactivat, activeu-lo ;)</string>
|
||||
<string name="overview_monitoring_active_label">Monitorització de dispositius</string>
|
||||
<string name="overview_monitoring_active_description">Assegureu-vos que el dispositiu estigui a prop i actiu.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Connectat, però sense dades</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">El dispositiu està connectat, però el CAPod no rep dades en directe. El telèfon pot necessitar una opció de compatibilitat: el solucionador de problemes pot intentar trobar-ne una automàticament.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Executa el solucionador de problemes</string>
|
||||
<string name="overview_unmatched_devices_label">Dispositius no coincidents</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d dispositiu sense perfil coincident</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Cal el permís següent:</string>
|
||||
<string name="permission_system_alert_window_label">Finestra d\'alerta del sistema</string>
|
||||
<string name="permission_system_alert_window_description">Permet que el CAPod dibuixi sobre altres aplicacions per fer possible la característica «Mostra emergent».</string>
|
||||
<string name="settings_monitor_mode_manual_label">Quan l\'aplicació està oberta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Quan el dispositiu està connectat</string>
|
||||
<string name="settings_monitor_mode_always_label">Sempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Quan es veu</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">La funda està oberta</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">A l\'orella</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Transparència</string>
|
||||
<string name="anc_mode_adaptive">Adaptatiu</string>
|
||||
<string name="conversation_awareness_label">Consciència de conversa</string>
|
||||
<string name="settings_conversation_action_label">Quan comenceu a parlar</string>
|
||||
<string name="settings_conversation_action_nothing_label">No fer res</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Baixa el volum</string>
|
||||
<string name="settings_conversation_action_pause_label">Posa en pausa el reproductor</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Reducció del volum</string>
|
||||
<string name="signal_badge_ble_cd">Escaneig BLE actiu</string>
|
||||
<string name="signal_badge_key_irk_cd">Identitat verificada</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Connexió xifrada</string>
|
||||
@@ -289,7 +300,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 mínima del senyal</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>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Ambre</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Utilitza els colors del fons de pantalla</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitoratge</string>
|
||||
<string name="settings_category_appearance_label">Aparença</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Canvis no desats</string>
|
||||
@@ -391,7 +401,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>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">General</string>
|
||||
<string name="device_settings_microphone_mode_label">Micròfon</string>
|
||||
<string name="device_settings_microphone_mode_description">Quin AirPod s\'utilitza com a micròfon</string>
|
||||
<string name="device_settings_microphone_mode_description">Quin auricular s\'utilitza com a micròfon</string>
|
||||
<string name="device_settings_microphone_mode_auto">Automàtic</string>
|
||||
<string name="device_settings_microphone_mode_right">Dreta</string>
|
||||
<string name="device_settings_microphone_mode_left">Esquerre</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Notificació de càrrega completa</string>
|
||||
<string name="reaction_charged_notification_title">Càrrega completada</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s està totalment carregat.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s s\'ha carregat fins al %2$d%%.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Pokud jste produkt zakoupili nedávno, synchronizace Google Play může chvíli trvat.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Pokud se váš nákup nezobrazí, zkuste to znovu za několik minut.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Ujistěte se, že jste přihlášeni ke stejnému účtu Google, který jste použili při nákupu.</string>
|
||||
<string name="settings_monitor_mode_label">Režim monitorování</string>
|
||||
<string name="settings_monitor_mode_description">Za jakých okolností tato aplikace monitoruje data Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Dodatečné oznámení</string>
|
||||
<string name="settings_monitor_connected_notification_description">Zobrazí dodatečné oznámení, když je zařízení připojeno. To umožňuje skrýt trvalé oznámení \"Žádné zařízení\" vypnutím kanálu \"Stav zařízení\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Uchovat oznámení po odpojení</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Autom. pozastavení</string>
|
||||
<string name="settings_autopause_description">Pozastavení zvuku při vyjmutí zařízení z ucha.</string>
|
||||
<string name="settings_autopplay_label">Autom. přehrávání</string>
|
||||
<string name="settings_autoplay_description">Spuštění přehrávání zvuku při nošení zařízení.</string>
|
||||
<string name="settings_autoplay_description">Obnoví přehrávání hudby po opětovném nasazení sluchátek, ale pouze pokud ji zastavilo Auto pozastavení. Pauzy, které jste spustili sami (telefon, ovladač AirPods, spánek), zůstanou pozastaveny.</string>
|
||||
<string name="settings_start_music_on_wear_label">Spustit hudbu při nasazení</string>
|
||||
<string name="settings_start_music_on_wear_description">Vždy se pokusí přehrát hudbu při nasazení sluchátek, i po ručním pozastavení.</string>
|
||||
<string name="settings_eardetection_info_label">Poznámka k detekci ucha</string>
|
||||
<string name="settings_eardetection_info_description">Pokud detekce sluchátek funguje pouze pro jedno sluchátko, jedná se o omezení společnosti Apple. Detekováno je pouze \"primární sluchátko\" (používané jako mikrofon). Konfigurace na zařízeních Apple: Nastavení → Bluetooth → AirPods → Mikrofon.</string>
|
||||
<string name="settings_fake_data_label">Falešná data</string>
|
||||
<string name="settings_fake_data_description">Zobrazit falešná data např. simulovat zařízení, které neexistuje.</string>
|
||||
<string name="settings_debug_label">Nastavení ladění</string>
|
||||
<string name="settings_debug_description">Další nastavení, která pomáhají řešit problémy s aplikací.</string>
|
||||
<string name="settings_signal_minimum_label">Minimální kvalita signálu</string>
|
||||
<string name="settings_signal_minimum_description">Minimální kvalita signálu, kterou musí zařízení mít, aby bylo považováno za vaše.</string>
|
||||
<string name="settings_autoconnect_label">Automatické připojení</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<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="general_fix_it_action">Opravit</string>
|
||||
<string name="settings_charged_notification_label">Oznámení o nabití</string>
|
||||
<string name="settings_charged_notification_description">Zobrazit oznámení, jakmile vše, co se nabíjí, dosáhne cílové úrovně nabití.</string>
|
||||
<string name="settings_charged_threshold_label">Cílová úroveň nabití</string>
|
||||
<string name="settings_charged_scope_label">Sledované baterie</string>
|
||||
<string name="settings_charged_scope_pods_label">Pouze sluchátka</string>
|
||||
<string name="settings_charged_scope_case_label">Pouze pouzdro</string>
|
||||
<string name="settings_charged_scope_both_label">Sluchátka a pouzdro</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ě, nahlaste problém s logem ladění.</string>
|
||||
<string name="device_settings_experimental_action">Otevřít sledovač problémů</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Ladění</string>
|
||||
<string name="settings_general_label">Nastavení</string>
|
||||
<string name="settings_general_description">Obecná vylepšení, která ovlivňují celou aplikaci.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Skrýt nepřiřazená zařízení</string>
|
||||
<string name="settings_overview_hide_unmatched_description">V přehledu nezobrazovat blízká zařízení, která neodpovídají žádnému z vašich profilů, např. AirPods jiných lidí.</string>
|
||||
<string name="settings_acknowledgements_label">Poděkování</string>
|
||||
<string name="settings_debug_autoreports_label">Automatické hlášení chyb</string>
|
||||
<string name="settings_debug_autoreports_description">Automatické hlášení chyb např. podrobnosti o pádech aplikace.</string>
|
||||
<string name="settings_debug_mode_label">Režim ladění</string>
|
||||
<string name="settings_debug_mode_description">Zobrazit další informace pro řešení problémů.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Nefiltrovaná data BLE</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Aby se zobrazovala všechna vysílaná data BLE, odstraňte ze skenování BLE všechny filtry. Užitečné pro přidání podpory nových typů sluchátek.</string>
|
||||
<string name="settings_compatibility_mode_label">Režim kompatibility</string>
|
||||
<string name="settings_compatibility_mode_description">Vypnutí optimalizace pro zlepšení kompatibility. Zkuste to, pokud se vám nezobrazují žádná data.</string>
|
||||
<string name="help_translate_label">Překlad</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Tento widget vyžaduje přímé připojení (AAP) k vašemu zařízení. Před použitím tohoto widgetu se ujistěte, že je vaše zařízení spárováno a připojeno v aplikaci.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Toto zařízení nepodporuje kontrolu hluku</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Nepřipojeno</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">V dosahu, ale nepřipojeno</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Není v dosahu</string>
|
||||
<string name="anc_widget_aap_connecting_label">Připojování…</string>
|
||||
<string name="tile_anc_label">Ovládání zvuku</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Vyžadováno oprávnění</string>
|
||||
<string name="tile_anc_subtitle_no_device">Žádné zařízení</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Žádné ovládání hluku</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth je vypnuto</string>
|
||||
<string name="widget_config_screen_title">Konfigurace widgetu</string>
|
||||
<string name="widget_configuration_title">Vybrat zařízení</string>
|
||||
<string name="widget_configuration_description">Vyberte, který profil zařízení má tento widget zobrazovat.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth je vypnuto, zapněte jej ;)</string>
|
||||
<string name="overview_monitoring_active_label">Monitorování zařízení</string>
|
||||
<string name="overview_monitoring_active_description">Ujistěte se, že je vaše zařízení v dosahu a aktivní.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Připojeno, ale žádná data</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Zařízení je připojeno, ale CAPod od něj nepřijímá žádná živá data. Váš telefon může potřebovat možnost kompatibility — řešení problémů ji může zkusit najít automaticky.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Spustit řešení problémů</string>
|
||||
<string name="overview_unmatched_devices_label">Nepřiřazená zařízení</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d zařízení bez odpovídajícího profilu</item>
|
||||
@@ -224,9 +233,6 @@
|
||||
<string name="permission_required_title">Je vyžadováno následující oprávnění:</string>
|
||||
<string name="permission_system_alert_window_label">Okno systémových upozornění</string>
|
||||
<string name="permission_system_alert_window_description">Aby bylo možné použít unkci \"Zobrazit vyskakovací okno\", je nutné umožnit aplikaci CAPod vykreslení přes jiné aplikace.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Při otevření aplikace</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Při připojení zařízení</string>
|
||||
<string name="settings_monitor_mode_always_label">Vždy</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Když je vidět</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Pouzdro je otevřeno</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">V uchu</string>
|
||||
@@ -258,6 +264,11 @@
|
||||
<string name="anc_mode_transparency">Průhlednost</string>
|
||||
<string name="anc_mode_adaptive">Adaptivní</string>
|
||||
<string name="conversation_awareness_label">Uvědomění si konverzace</string>
|
||||
<string name="settings_conversation_action_label">Když začnete mluvit</string>
|
||||
<string name="settings_conversation_action_nothing_label">Nedělat nic</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Snížit hlasitost</string>
|
||||
<string name="settings_conversation_action_pause_label">Pozastavit média</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Snížení hlasitosti</string>
|
||||
<string name="signal_badge_ble_cd">BLE skenování aktivní</string>
|
||||
<string name="signal_badge_key_irk_cd">Identita ověřena</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Šifrované připojení</string>
|
||||
@@ -326,7 +337,6 @@
|
||||
<string name="ui_theme_color_amber_label">Jantarová</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Používá barvy tapety</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitorování</string>
|
||||
<string name="settings_category_appearance_label">Vzhled</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Neuložené změny</string>
|
||||
@@ -421,8 +431,8 @@
|
||||
<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_left_bonded_label">Levé spárováno</string>
|
||||
<string name="device_settings_info_right_bonded_label">Pravé spárováno</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>
|
||||
@@ -460,48 +470,52 @@
|
||||
<string name="device_settings_press_hold_default">Výchozí</string>
|
||||
<string name="device_settings_press_hold_shorter">Kratší</string>
|
||||
<string name="device_settings_press_hold_shortest">Nejkratší</string>
|
||||
<string name="device_settings_volume_swipe_label">Přejetí pro hlasitost</string>
|
||||
<string name="device_settings_volume_swipe_label">Hlasitost přejetím</string>
|
||||
<string name="device_settings_volume_swipe_description">Přejetím upravte hlasitost</string>
|
||||
<string name="device_settings_volume_swipe_length_label">Čekací doba tažení pro hlasitost</string>
|
||||
<string name="device_settings_volume_swipe_length_description">Chcete-li zabránit nechtěným úpravám hlasitosti, zvolte, jak dlouho čekat mezi tahy.</string>
|
||||
<string name="device_settings_volume_swipe_length_label">Doba čekání hlasitosti přejetím</string>
|
||||
<string name="device_settings_volume_swipe_length_description">Chcete-li zabránit nechtěným úpravám hlasitosti, zvolte, jak dlouho čekat mezi přejetími.</string>
|
||||
<string name="device_settings_volume_swipe_length_default">Výchozí</string>
|
||||
<string name="device_settings_volume_swipe_length_longer">Delší</string>
|
||||
<string name="device_settings_volume_swipe_length_longest">Nejdelší</string>
|
||||
<string name="device_settings_end_call_mute_mic_label">Ovládání hovorů</string>
|
||||
<string name="device_settings_end_call_mute_mic_description">Zvolte, co dělá stisk stonku během hovorů</string>
|
||||
<string name="device_settings_end_call_mute_mic_option_a_title">Jednorázový stisk pro ukončení hovoru</string>
|
||||
<string name="device_settings_end_call_mute_mic_description">Vyberte, jaká akce se má spustit stisknutím tlačítka během hovoru</string>
|
||||
<string name="device_settings_end_call_mute_mic_option_a_title">Jeden stisk pro ukončení hovoru</string>
|
||||
<string name="device_settings_end_call_mute_mic_option_a_subtitle">Dvojitý stisk pro ztlumení mikrofonu</string>
|
||||
<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_title">Jeden 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">Tento profil nemá spárované zařízení Bluetooth.</string>
|
||||
<string name="overview_reactions_hint_title">Reakce jsou nastaveny pro každé zařízení</string>
|
||||
<string name="overview_reactions_hint_body">Automatické přehrávání, automatické pozastavení a vyskakovací okna jsou nyní konfigurována pro každé zařízení. Klepněte na ikonu nastavení na kartě, když jsou vaše sluchátka připojena.</string>
|
||||
<string name="overview_reactions_hint_body">Automatické přehrávání, automatické pozastavení a vyskakovací okna se nyní nastavují pro každé zařízení zvlášť. Klepněte na ikonu nastavení na kartě, když máte připojená sluchátka.</string>
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Obecné</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_description">Který AirPod je použit jako mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_auto">Auto</string>
|
||||
<string name="device_settings_microphone_mode_description">Které sluchátko je použito jako mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_auto">Autom.</string>
|
||||
<string name="device_settings_microphone_mode_right">Pravý</string>
|
||||
<string name="device_settings_microphone_mode_left">Levý</string>
|
||||
<string name="device_settings_noise_control_current_mode_label">Aktuální režim</string>
|
||||
<string name="device_settings_listening_mode_cycle_label">Cyklus dlouhého stisku</string>
|
||||
<string name="device_settings_listening_mode_cycle_description">Vyberte, které režimy jsou dostupné při držení stopky a v rychlém přepínači.</string>
|
||||
<string name="device_settings_listening_mode_cycle_description">Vyberte, které režimy budou k dispozici při podržení tlačítka a v rychlém přepínači.</string>
|
||||
<string name="device_settings_listening_mode_cycle_dialog_title">Přizpůsobit cyklus dlouhého stisku</string>
|
||||
<string name="device_settings_listening_mode_cycle_summary_helper">Používá se při držení stopky a v rychlém přepínači</string>
|
||||
<string name="device_settings_listening_mode_cycle_summary_helper_override">Používá se v rychlém přepínači. Vlastní akce dlouhého stisku stopky přepisuje cyklování AirPods.</string>
|
||||
<string name="device_settings_listening_mode_cycle_summary_helper">Používá se při držení tlačítka a v rychlém přepínači</string>
|
||||
<string name="device_settings_listening_mode_cycle_summary_helper_override">Používá se v rychlém přepínači. Vlastní akce dlouhého stisku přepínače přepisuje cyklování AirPods.</string>
|
||||
<string name="device_settings_listening_mode_cycle_minimum">Musí zůstat povoleny alespoň 2 režimy.</string>
|
||||
<string name="device_settings_listening_mode_cycle_off">Vypnuto</string>
|
||||
<string name="device_settings_listening_mode_cycle_off">Vyp.</string>
|
||||
<string name="device_settings_listening_mode_cycle_anc">Potlačení hluku</string>
|
||||
<string name="device_settings_listening_mode_cycle_transparency">Průhlednost</string>
|
||||
<string name="device_settings_listening_mode_cycle_transparency">Propustnost</string>
|
||||
<string name="device_settings_listening_mode_cycle_adaptive">Adaptivní</string>
|
||||
<string name="device_settings_allow_off_label">Povolit režim Vypnuto</string>
|
||||
<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_allow_off_description">Povolit Vypnuto jako volitelný režim kontroly hluku. Pokud je zakázáno, tlačítka 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="reaction_charged_channel_label">Oznámení o nabití</string>
|
||||
<string name="reaction_charged_notification_title">Nabíjení dokončeno</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s je plně nabitý.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s byl nabit na %2$d%%.</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>
|
||||
@@ -510,7 +524,7 @@
|
||||
<string name="device_settings_rename_system_unavailable">Android nám nedovolil přejmenovat zařízení zde. Chcete-li aktualizovat název v nastavení Bluetooth, přejmenujte ho tam nebo zařízení znovu spárujte.</string>
|
||||
<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_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>
|
||||
@@ -530,7 +544,7 @@
|
||||
<string name="press_controls_double_press">Dvojitý stisk</string>
|
||||
<string name="press_controls_triple_press">Trojitý stisk</string>
|
||||
<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_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, propustnost) 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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Hvis du for nylig har købt, kan det tage et øjeblik for Google Play at synkronisere.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Prøv igen om et par minutter, hvis dit køb ikke vises.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Sørg for, at du er logget ind med den samme Google-konto, der blev brugt til købet.</string>
|
||||
<string name="settings_monitor_mode_label">Overvågningstilstand</string>
|
||||
<string name="settings_monitor_mode_description">Under hvilke omstændigheder denne app overvåger Bluetooth-data.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Ekstra notifikation</string>
|
||||
<string name="settings_monitor_connected_notification_description">Viser en ekstra notifikation, når en enhed er tilsluttet. Dette lader dig skjule den permanente \"Ingen enheder\"-notifikation ved at deaktivere \"Enhedsstatus\"-kanalen.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Behold notifikation efter afbrydelse</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Auto-pause</string>
|
||||
<string name="settings_autopause_description">Sæt lyd på pause, når du fjerner enheden fra dit øre.</string>
|
||||
<string name="settings_autopplay_label">Auto-afspilning</string>
|
||||
<string name="settings_autoplay_description">Start lydafspilning, når enheden bæres.</string>
|
||||
<string name="settings_autoplay_description">Genoptag musik, når du sætter dine pods på igen, men kun hvis Auto pause stoppede det. Pauser du selv udløste (telefon, AirPods-stift, søvn) forbliver sat på pause.</string>
|
||||
<string name="settings_start_music_on_wear_label">Start musik ved iføring</string>
|
||||
<string name="settings_start_music_on_wear_description">Forsøger altid at afspille musik, når du sætter dine pods på, selv efter en manuel pause.</string>
|
||||
<string name="settings_eardetection_info_label">Bemærkning om øredetektion</string>
|
||||
<string name="settings_eardetection_info_description">Hvis øredetektion kun virker for én pod, er dette en Apple-begrænsning. Kun den \"primære pod\" (brugt til mikrofon) detekteres. Konfigurer på Apple-enheder: Indstillinger → Bluetooth → AirPods → Mikrofon.</string>
|
||||
<string name="settings_fake_data_label">Falske data</string>
|
||||
<string name="settings_fake_data_description">Vis falske data, dvs. simuler enheder, der ikke eksisterer.</string>
|
||||
<string name="settings_debug_label">Fejlfindingsindstillinger</string>
|
||||
<string name="settings_debug_description">Yderligere indstillinger til at hjælpe med at fejlfinde problemer med appen.</string>
|
||||
<string name="settings_signal_minimum_label">Minimum signalkvalitet</string>
|
||||
<string name="settings_signal_minimum_description">Den minimum signalkvalitet, en enhed skal have for at blive betragtet som din.</string>
|
||||
<string name="settings_autoconnect_label">Auto-tilslut</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Vis forbindelses-pop op</string>
|
||||
<string name="settings_popup_connected_description">Vis en pop op, når enheden tilsluttes for første gang.</string>
|
||||
<string name="settings_popup_info_not_in_app">Pop-ups vises kun, når du ikke er i appen.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Pop-ups kræver, at monitoren kører. Din monitortilstand er indstillet til \"Når appen er åben\", som stopper monitoren, når du forlader appen.</string>
|
||||
<string name="general_fix_it_action">Ret det</string>
|
||||
<string name="settings_charged_notification_label">Opladt notifikation</string>
|
||||
<string name="settings_charged_notification_description">Vis en notifikation, når alt der oplades når det ønskede opladningsniveau.</string>
|
||||
<string name="settings_charged_threshold_label">Målopladningsniveau</string>
|
||||
<string name="settings_charged_scope_label">Overvågede batterier</string>
|
||||
<string name="settings_charged_scope_pods_label">Kun pods</string>
|
||||
<string name="settings_charged_scope_case_label">Kun etui</string>
|
||||
<string name="settings_charged_scope_both_label">Pods og etui</string>
|
||||
<string name="device_settings_experimental_title">Eksperimentel funktion</string>
|
||||
<string name="device_settings_experimental_description">Denne funktion er endnu ikke testet på alle enheder. Hvis noget ikke fungerer korrekt, bedes du åbne en sag med en fejlretningslog.</string>
|
||||
<string name="device_settings_experimental_action">Åbn fejlsporer</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Fejlfinding</string>
|
||||
<string name="settings_general_label">Indstillinger</string>
|
||||
<string name="settings_general_description">Generelle justeringer, der påvirker hele appen.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Skjul ikke-matchende enheder</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Vis ikke nærliggende enheder, der ikke matcher nogen af dine profiler i overblikkket, f.eks. andres AirPods.</string>
|
||||
<string name="settings_acknowledgements_label">Anerkendelser</string>
|
||||
<string name="settings_debug_autoreports_label">Automatiske fejlrapporter</string>
|
||||
<string name="settings_debug_autoreports_description">Rapporterer automatisk problemer, f.eks. detaljer om et app-nedbrud, så jeg kan finde ud af, hvordan jeg løser det.</string>
|
||||
<string name="settings_debug_mode_label">Fejlfindingstilstand</string>
|
||||
<string name="settings_debug_mode_description">Vis yderligere oplysninger for at fejlfinde problemer.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Ufiltrerede BLE-data</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Fjern alle filtre fra BLE-scanneren for at vise alle udsendte BLE-data. Nyttigt til at tilføje understøttelse af nye hovedtelefontyper.</string>
|
||||
<string name="settings_compatibility_mode_label">Kompatibilitetstilstand</string>
|
||||
<string name="settings_compatibility_mode_description">Deaktiver optimeringer for at forbedre kompatibiliteten. Prøv dette, hvis du ikke ser nogen data.</string>
|
||||
<string name="help_translate_label">Oversættelse</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Denne widget kræver en direkte forbindelse (AAP) til din enhed. Sørg for, at din enhed er parret og forbundet i appen, inden du bruger denne widget.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Denne enhed understøtter ikke støjkontrol</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Ikke forbundet</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">I nærheden, men ikke tilsluttet</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Ikke i nærheden</string>
|
||||
<string name="anc_widget_aap_connecting_label">Forbinder…</string>
|
||||
<string name="tile_anc_label">Støjstyring</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Tilladelse påkrævet</string>
|
||||
<string name="tile_anc_subtitle_no_device">Ingen enhed</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Ingen støjstyring</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth slået fra</string>
|
||||
<string name="widget_config_screen_title">Widget-konfiguration</string>
|
||||
<string name="widget_configuration_title">Vælg enhed</string>
|
||||
<string name="widget_configuration_description">Vælg hvilken enhedsprofil denne widget skal vise.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth er deaktiveret, aktivér det ;)</string>
|
||||
<string name="overview_monitoring_active_label">Overvåger for enheder</string>
|
||||
<string name="overview_monitoring_active_description">Sørg for, at din enhed er i nærheden og aktiv.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Forbundet, men ingen data</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Din enhed er forbundet, men CAPod modtager ikke live data fra den. Din telefon har måske brug for en kompatibilitetsindstilling — fejlfinderen kan prøve at finde en automatisk.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Kør fejlfinder</string>
|
||||
<string name="overview_unmatched_devices_label">Uparrede enheder</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d enhed uden matchende profil</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Følgende tilladelse er påkrævet:</string>
|
||||
<string name="permission_system_alert_window_label">Systemalarmvindue</string>
|
||||
<string name="permission_system_alert_window_description">Tillad CAPod at tegne over andre apps for at gøre funktionen \"Vis pop op\" mulig.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Når appen er åben</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Når enheden er tilsluttet</string>
|
||||
<string name="settings_monitor_mode_always_label">Altid</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Når den ses</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Etui er åbent</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">I øret</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Gennemsigtighed</string>
|
||||
<string name="anc_mode_adaptive">Adaptiv</string>
|
||||
<string name="conversation_awareness_label">Samtaleopmærksomhed</string>
|
||||
<string name="settings_conversation_action_label">Når du begynder at tale</string>
|
||||
<string name="settings_conversation_action_nothing_label">Gør intet</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Sænk lydstyrken</string>
|
||||
<string name="settings_conversation_action_pause_label">Sæt medie på pause</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Lydstyrkeregulering</string>
|
||||
<string name="signal_badge_ble_cd">BLE-scanning aktiv</string>
|
||||
<string name="signal_badge_key_irk_cd">Identitet bekræftet</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Krypteret forbindelse</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Rav</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Bruger tapetkfarver</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Overvågning</string>
|
||||
<string name="settings_category_appearance_label">Udseende</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Ikke-gemte ændringer</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Generel</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_description">Hvilken AirPod der bruges som mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_description">Hvilken øretelefon der bruges som mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_auto">Auto</string>
|
||||
<string name="device_settings_microphone_mode_right">Højre</string>
|
||||
<string name="device_settings_microphone_mode_left">Venstre</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Opladt notifikation</string>
|
||||
<string name="reaction_charged_notification_title">Opladning færdig</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s er fuldt opladet.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s er blevet opladet til %2$d%%.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Wenn du kürzlich gekauft hast, kann es einen Moment dauern, bis Google Play synchronisiert.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Versuche es in wenigen Minuten erneut, falls dein Kauf nicht erscheint.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Stelle sicher, dass du mit demselben Google-Konto angemeldet bist, das für den Kauf verwendet wurde.</string>
|
||||
<string name="settings_monitor_mode_label">Überwachungsmodus</string>
|
||||
<string name="settings_monitor_mode_description">Unter welchen Umständen überwacht diese App Bluetooth-Daten.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Zusätzliche Benachrichtigung</string>
|
||||
<string name="settings_monitor_connected_notification_description">Zeigt eine zusätzliche Benachrichtigung an, wenn ein Gerät verbunden ist. Dadurch kannst du die permanente Benachrichtigung „Keine Geräte“ ausblenden, indem du den Kanal „Gerätestatus“ deaktivierst.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Benachrichtigung nach Trennung beibehalten</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Automatische Pause</string>
|
||||
<string name="settings_autopause_description">Musik pausieren, wenn der Kopfhörer vom Ohr entfernt wird.</string>
|
||||
<string name="settings_autopplay_label">Automatisches abspielen</string>
|
||||
<string name="settings_autoplay_description">Audiowiedergabe starten, wenn das Gerät getragen wird.</string>
|
||||
<string name="settings_autoplay_description">Musik fortsetzen, wenn du deine Pods wieder trägst – aber nur, wenn Auto-Pause sie gestoppt hat. Pausen, die du selbst ausgelöst hast (Telefon, AirPods-Stiel, Schlaf), bleiben aktiv.</string>
|
||||
<string name="settings_start_music_on_wear_label">Musik beim Anlegen starten</string>
|
||||
<string name="settings_start_music_on_wear_description">Versucht immer, Musik abzuspielen, wenn du deine Pods aufsetzt – auch nach einer manuellen Pause.</string>
|
||||
<string name="settings_eardetection_info_label">Hinweis zur Ohr-Erkennung</string>
|
||||
<string name="settings_eardetection_info_description">Wenn die Ohr-Erkennung nur für einen Pod funktioniert, ist das leider Apple-Einschränkung. Es wird dann nur der \"primäre Pod\" (Mikrofon) erkannt. Weitere Konfiguration auf Apple-Geräten: Einstellungen → Bluetooth → AirPods → Mikrofon.</string>
|
||||
<string name="settings_fake_data_label">Gefälschte Daten</string>
|
||||
<string name="settings_fake_data_description">Gefälschte Test-Daten anzeigen, d.h. nicht existierende Geräte simulieren.</string>
|
||||
<string name="settings_debug_label">Debug-Einstellungen</string>
|
||||
<string name="settings_debug_description">Zusätzliche Einstellungen zur Behebung von Problemen mit der App.</string>
|
||||
<string name="settings_signal_minimum_label">Minimale Signalqualität</string>
|
||||
<string name="settings_signal_minimum_description">Die minimale Signalqualität, die ein Gerät haben muss, damit als Deins erkannt wird.</string>
|
||||
<string name="settings_autoconnect_label">Automatisch verbinden</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Verbindungs-Popup anzeigen</string>
|
||||
<string name="settings_popup_connected_description">Zeigt ein Popup an, wenn das Gerät zum ersten Mal eine Verbindung herstellt.</string>
|
||||
<string name="settings_popup_info_not_in_app">Popups werden nur angezeigt, wenn du nicht in der App bist.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Popups erfordern, dass der Monitor läuft. Dein Monitor-Modus ist auf \"Wenn App geöffnet\" eingestellt, was den Monitor stoppt, wenn du die App verlässt.</string>
|
||||
<string name="general_fix_it_action">Beheben</string>
|
||||
<string name="settings_charged_notification_label">Aufgeladen-Benachrichtigung</string>
|
||||
<string name="settings_charged_notification_description">Zeigt eine Benachrichtigung an, sobald alles, was geladen wird, den Ziel-Ladestand erreicht.</string>
|
||||
<string name="settings_charged_threshold_label">Ziel-Ladestand</string>
|
||||
<string name="settings_charged_scope_label">Überwachte Akkus</string>
|
||||
<string name="settings_charged_scope_pods_label">Nur Pods</string>
|
||||
<string name="settings_charged_scope_case_label">Nur Case</string>
|
||||
<string name="settings_charged_scope_both_label">Pods und Case</string>
|
||||
<string name="device_settings_experimental_title">Experimentelle Funktion</string>
|
||||
<string name="device_settings_experimental_description">Diese Funktion wurde noch nicht auf allen Geräten getestet. Wenn etwas nicht korrekt funktioniert, öffne bitte ein Issue mit einem Debug-Log.</string>
|
||||
<string name="device_settings_experimental_action">Issue-Tracker öffnen</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Debug</string>
|
||||
<string name="settings_general_label">Einstellungen</string>
|
||||
<string name="settings_general_description">Allgemeine Optimierungen, die die gesamte App betreffen.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Nicht zugeordnete Geräte ausblenden</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Geräte in der Nähe, die keinem deiner Profile entsprechen, werden in der Übersicht nicht angezeigt, z. B. AirPods anderer Personen.</string>
|
||||
<string name="settings_acknowledgements_label">Danksagungen</string>
|
||||
<string name="settings_debug_autoreports_label">Automatische Fehlerberichte</string>
|
||||
<string name="settings_debug_autoreports_description">Meldet Probleme automatisch, z.B. Details zu einem App-Absturz, damit ich herausfinden kann, wie ich ihn beheben kann.</string>
|
||||
<string name="settings_debug_mode_label">Debug-Modus</string>
|
||||
<string name="settings_debug_mode_description">Zeige zusätzliche Informationen an, um Probleme zu beheben.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Ungefilterte BLE-Daten</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Entfernt alle Filter aus dem BLE-Scanner, um alle übertragenen BLE-Daten anzuzeigen. Nützlich, um Unterstützung für neue Kopfhörertypen hinzuzufügen.</string>
|
||||
<string name="settings_compatibility_mode_label">Kompatibilitätsmodus</string>
|
||||
<string name="settings_compatibility_mode_description">Deaktiviere Optimierungen, um die Kompatibilität zu verbessern. Probier diese Option, wenn gar keine Daten angezeigt werden.</string>
|
||||
<string name="help_translate_label">Übersetzung</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Dieses Widget erfordert eine direkte Verbindung (AAP) zu deinem Gerät. Stelle sicher, dass dein Gerät in der App gekoppelt und verbunden ist, bevor du dieses Widget verwendest.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Dieses Gerät unterstützt keine Geräuschsteuerung</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Nicht verbunden</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">In der Nähe, aber nicht verbunden</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Nicht in der Nähe</string>
|
||||
<string name="anc_widget_aap_connecting_label">Verbinde…</string>
|
||||
<string name="tile_anc_label">Geräuschkontrolle</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Berechtigung erforderlich</string>
|
||||
<string name="tile_anc_subtitle_no_device">Kein Gerät</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Keine Geräuschkontrolle</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth aus</string>
|
||||
<string name="widget_config_screen_title">Widget-Konfiguration</string>
|
||||
<string name="widget_configuration_title">Gerät auswählen</string>
|
||||
<string name="widget_configuration_description">Wähle aus, welches Geräteprofil dieses Widget anzeigen soll.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
|
||||
<string name="overview_monitoring_active_label">Überwachung für Geräte</string>
|
||||
<string name="overview_monitoring_active_description">Stelle sicher, dass dein Gerät in der Nähe ist und aktiv ist.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Verbunden, aber keine Daten</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Dein Gerät ist verbunden, aber CAPod empfängt keine Live-Daten davon. Dein Telefon benötigt möglicherweise eine Kompatibilitätsoption – die Fehlerbehebung kann versuchen, automatisch eine zu finden.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Fehlerbehebung starten</string>
|
||||
<string name="overview_unmatched_devices_label">Nicht zugeordnete Geräte</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d Gerät ohne passendes Profil</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Die folgende Genehmigung ist erforderlich:</string>
|
||||
<string name="permission_system_alert_window_label">Systemwarnungsfenster</string>
|
||||
<string name="permission_system_alert_window_description">Erlaube CAPod, PopUp Benachrichtigungen über anderen Apps anzuzeigen.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Wenn die App geöffnet ist</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Wenn das Gerät verbunden ist</string>
|
||||
<string name="settings_monitor_mode_always_label">Immer</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Wann gesehen</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Ladecase ist offen</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Im Ohr</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Transparenz</string>
|
||||
<string name="anc_mode_adaptive">Adaptiv</string>
|
||||
<string name="conversation_awareness_label">Gesprächserkennung</string>
|
||||
<string name="settings_conversation_action_label">Wenn du anfängst zu sprechen</string>
|
||||
<string name="settings_conversation_action_nothing_label">Nichts tun</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Lautstärke verringern</string>
|
||||
<string name="settings_conversation_action_pause_label">Medien pausieren</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Lautstärkereduzierung</string>
|
||||
<string name="signal_badge_ble_cd">BLE-Scan aktiv</string>
|
||||
<string name="signal_badge_key_irk_cd">Identität verifiziert</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Verschlüsselte Verbindung</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Bernstein</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Verwendet Hintergrundbildfarben</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Überwachung</string>
|
||||
<string name="settings_category_appearance_label">Erscheinungsbild</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Nicht gespeicherte Änderungen</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Allgemein</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_description">Welcher AirPod als Mikrofon verwendet wird</string>
|
||||
<string name="device_settings_microphone_mode_description">Welches Ohrstöpsel als Mikrofon verwendet wird</string>
|
||||
<string name="device_settings_microphone_mode_auto">Automatisch</string>
|
||||
<string name="device_settings_microphone_mode_right">Rechts</string>
|
||||
<string name="device_settings_microphone_mode_left">Links</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Aufgeladen-Benachrichtigung</string>
|
||||
<string name="reaction_charged_notification_title">Laden abgeschlossen</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s ist vollständig geladen.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s wurde auf %2$d%% geladen.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Εάν έχετε αγοράσει πρόσφατα, ενδέχεται να χρειαστεί λίγος χρόνος για συγχρονισμό με το Google Play.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Δοκιμάστε ξανά σε λίγα λεπτά αν η αγορά σας δεν εμφανιστεί.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Βεβαιωθείτε ότι έχετε συνδεθεί με τον ίδιο λογαριασμό Google που χρησιμοποιήσατε για την αγορά.</string>
|
||||
<string name="settings_monitor_mode_label">Λειτουργία παρακολούθησης</string>
|
||||
<string name="settings_monitor_mode_description">Υπό ποιες συνθήκες αυτή η εφαρμογή παρακολουθεί δεδομένα Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Επιπλέον ειδοποίηση</string>
|
||||
<string name="settings_monitor_connected_notification_description">Εμφανίζει μια επιπλέον ειδοποίηση όταν μια συσκευή είναι συνδεδεμένη. Αυτό σας επιτρέπει να αποκρύψετε τη μόνιμη ειδοποίηση \"Δεν υπάρχουν συσκευές\" απενεργοποιώντας το κανάλι \"Κατάσταση συσκευής\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Διατήρηση ειδοποίησης μετά την αποσύνδεση</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Αυτόματη παύση</string>
|
||||
<string name="settings_autopause_description">Παύση ήχου κατά την αφαίρεση της συσκευής από το αυτί σας.</string>
|
||||
<string name="settings_autopplay_label">Αυτόματη αναπαραγωγή</string>
|
||||
<string name="settings_autoplay_description">Έναρξη αναπαραγωγής ήχου όταν φοριέται η συσκευή.</string>
|
||||
<string name="settings_autoplay_description">Συνέχισε τη μουσική όταν ξαναφοράς τα pods, αλλά μόνο αν την σταμάτησε η Αυτόματη παύση. Οι παύσεις που έκανες εσύ (τηλέφωνο, στέλεχος AirPods, ύπνος) παραμένουν σε παύση.</string>
|
||||
<string name="settings_start_music_on_wear_label">Εκκίνηση μουσικής κατά τη χρήση</string>
|
||||
<string name="settings_start_music_on_wear_description">Προσπαθεί πάντα να αναπαράγει μουσική όταν φοράς τα pods, ακόμα και μετά από μη αυτόματη παύση.</string>
|
||||
<string name="settings_eardetection_info_label">Σημείωση ανίχνευσης αυτιού</string>
|
||||
<string name="settings_eardetection_info_description">Εάν η ανίχνευση αυτιού λειτουργεί μόνο για ένα pod, αυτός είναι περιορισμός της Apple. Ανιχνεύεται μόνο το \"κύριο pod\" (που χρησιμοποιείται για μικρόφωνο). Ρυθμίστε σε συσκευές Apple: Ρυθμίσεις → Bluetooth → AirPods → Μικρόφωνο.</string>
|
||||
<string name="settings_fake_data_label">Ψευδή δεδομένα</string>
|
||||
<string name="settings_fake_data_description">Εμφάνιση ψευδών δεδομένων, π.χ., προσομοίωση συσκευών που δεν υπάρχουν.</string>
|
||||
<string name="settings_debug_label">Ρυθμίσεις εντοπισμού σφαλμάτων</string>
|
||||
<string name="settings_debug_description">Πρόσθετες ρυθμίσεις για την αντιμετώπιση προβλημάτων με την εφαρμογή.</string>
|
||||
<string name="settings_signal_minimum_label">Ελάχιστη ποιότητα σήματος</string>
|
||||
<string name="settings_signal_minimum_description">Η ελάχιστη ποιότητα σήματος που πρέπει να έχει μια συσκευή για να θεωρηθεί δική σας.</string>
|
||||
<string name="settings_autoconnect_label">Αυτόματη σύνδεση</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Εμφάνιση αναδυόμενου παραθύρου σύνδεσης</string>
|
||||
<string name="settings_popup_connected_description">Εμφάνιση αναδυόμενου παραθύρου όταν η συσκευή συνδέεται για πρώτη φορά.</string>
|
||||
<string name="settings_popup_info_not_in_app">Τα αναδυόμενα παράθυρα εμφανίζονται μόνο όταν δεν βρίσκεστε στην εφαρμογή.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Τα αναδυόμενα παράθυρα απαιτούν η παρακολούθηση να εκτελείται. Η λειτουργία παρακολούθησης έχει οριστεί σε «Όταν η εφαρμογή είναι ανοιχτή», η οποία διακόπτει την παρακολούθηση όταν φεύγετε από την εφαρμογή.</string>
|
||||
<string name="general_fix_it_action">Διόρθωση</string>
|
||||
<string name="settings_charged_notification_label">Ειδοποίηση φόρτισης</string>
|
||||
<string name="settings_charged_notification_description">Εμφάνιση ειδοποίησης μόλις όλα τα στοιχεία που φορτίζουν φτάσουν στο επιθυμητό επίπεδο φόρτισης.</string>
|
||||
<string name="settings_charged_threshold_label">Επιθυμητό επίπεδο φόρτισης</string>
|
||||
<string name="settings_charged_scope_label">Παρακολουθούμενες μπαταρίες</string>
|
||||
<string name="settings_charged_scope_pods_label">Μόνο ακουστικά</string>
|
||||
<string name="settings_charged_scope_case_label">Μόνο θήκη</string>
|
||||
<string name="settings_charged_scope_both_label">Ακουστικά και θήκη</string>
|
||||
<string name="device_settings_experimental_title">Πειραματική λειτουργία</string>
|
||||
<string name="device_settings_experimental_description">Αυτή η λειτουργία δεν έχει δοκιμαστεί ακόμα σε όλες τις συσκευές. Αν κάτι δεν λειτουργεί σωστά, παρακαλώ ανοίξτε ένα ζήτημα με ένα αρχείο καταγραφής εντοπισμού σφαλμάτων.</string>
|
||||
<string name="device_settings_experimental_action">Άνοιγμα Παρακολούθησης Ζητημάτων</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Αποσφαλμάτωση</string>
|
||||
<string name="settings_general_label">Ρυθμίσεις</string>
|
||||
<string name="settings_general_description">Γενικές τροποποιήσεις που επηρεάζουν ολόκληρη την εφαρμογή.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Απόκρυψη μη αντιστοιχισμένων συσκευών</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Να μην εμφανίζονται κοντινές συσκευές που δεν αντιστοιχούν σε κανένα από τα προφίλ σου στην επισκόπηση, π.χ. τα AirPods άλλων ανθρώπων.</string>
|
||||
<string name="settings_acknowledgements_label">Ευχαριστίες</string>
|
||||
<string name="settings_debug_autoreports_label">Αυτόματες αναφορές σφαλμάτων</string>
|
||||
<string name="settings_debug_autoreports_description">Αναφέρει αυτόματα προβλήματα, π.χ. λεπτομέρειες σχετικά με μια κατάρρευση της εφαρμογής, ώστε να μπορώ να καταλάβω πώς να το διορθώσω.</string>
|
||||
<string name="settings_debug_mode_label">Λειτουργία εντοπισμού σφαλμάτων</string>
|
||||
<string name="settings_debug_mode_description">Εμφάνιση πρόσθετων πληροφοριών για την αντιμετώπιση προβλημάτων.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Αφιλτράριστα δεδομένα BLE</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Αφαιρέστε τυχόν φίλτρα από τον σαρωτή BLE για να εμφανιστούν όλα τα μεταδιδόμενα δεδομένα BLE. Χρήσιμο για την προσθήκη υποστήριξης για νέους τύπους ακουστικών.</string>
|
||||
<string name="settings_compatibility_mode_label">Λειτουργία συμβατότητας</string>
|
||||
<string name="settings_compatibility_mode_description">Απενεργοποιήστε τις βελτιστοποιήσεις για να βελτιώσετε τη συμβατότητα. Δοκιμάστε το εάν δεν βλέπετε δεδομένα.</string>
|
||||
<string name="help_translate_label">Μετάφραση</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Αυτό το widget απαιτεί άμεση σύνδεση (AAP) με τη συσκευή σας. Βεβαιωθείτε ότι η συσκευή σας είναι συζευγμένη και συνδεδεμένη στην εφαρμογή πριν χρησιμοποιήσετε αυτό το widget.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Αυτή η συσκευή δεν υποστηρίζει έλεγχο θορύβου</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Μη συνδεδεμένο</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Κοντά αλλά δεν είναι συνδεδεμένο</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Μη κοντά</string>
|
||||
<string name="anc_widget_aap_connecting_label">Σύνδεση…</string>
|
||||
<string name="tile_anc_label">Έλεγχος θορύβου</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Απαιτείται άδεια</string>
|
||||
<string name="tile_anc_subtitle_no_device">Καμία συσκευή</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Χωρίς έλεγχο θορύβου</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth απενεργοποιημένο</string>
|
||||
<string name="widget_config_screen_title">Διαμόρφωση γραφικού στοιχείου</string>
|
||||
<string name="widget_configuration_title">Επιλογή Συσκευής</string>
|
||||
<string name="widget_configuration_description">Επιλέξτε ποιο προφίλ συσκευής θα εμφανίζει αυτό το widget.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Το Bluetooth είναι απενεργοποιημένο, ενεργοποιήστε το ;)</string>
|
||||
<string name="overview_monitoring_active_label">Παρακολούθηση συσκευών</string>
|
||||
<string name="overview_monitoring_active_description">Βεβαιωθείτε ότι η συσκευή σας είναι κοντά και ενεργή.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Συνδέθηκε, αλλά χωρίς δεδομένα</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Η συσκευή σου είναι συνδεδεμένη, αλλά το CAPod δεν λαμβάνει ζωντανά δεδομένα από αυτήν. Το τηλέφωνό σου μπορεί να χρειαστεί μια επιλογή συμβατότητας — ο εντοπισμός προβλημάτων μπορεί να προσπαθήσει να βρει μία αυτόματα.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Εκτέλεση εντοπισμού προβλημάτων</string>
|
||||
<string name="overview_unmatched_devices_label">Μη αντιστοιχισμένες συσκευές</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d συσκευή χωρίς αντίστοιχο προφίλ</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Απαιτείται η ακόλουθη άδεια:</string>
|
||||
<string name="permission_system_alert_window_label">Παράθυρο ειδοποίησης συστήματος</string>
|
||||
<string name="permission_system_alert_window_description">Επιτρέψτε στο CAPod να σχεδιάζει πάνω από άλλες εφαρμογές για να καταστεί δυνατή η λειτουργία \"Εμφάνιση αναδυόμενου\".</string>
|
||||
<string name="settings_monitor_mode_manual_label">Όταν η εφαρμογή είναι ανοιχτή</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Όταν η συσκευή είναι συνδεδεμένη</string>
|
||||
<string name="settings_monitor_mode_always_label">Πάντα</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Όταν εντοπιστεί</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Η θήκη είναι ανοιχτή</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Στο αυτί</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Διαφάνεια</string>
|
||||
<string name="anc_mode_adaptive">Προσαρμοστικό</string>
|
||||
<string name="conversation_awareness_label">Ενημερότητα Συνομιλίας</string>
|
||||
<string name="settings_conversation_action_label">Όταν αρχίζεις να μιλάς</string>
|
||||
<string name="settings_conversation_action_nothing_label">Να μην γίνει τίποτα</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Μείωση έντασης</string>
|
||||
<string name="settings_conversation_action_pause_label">Παύση μέσων</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Μείωση έντασης</string>
|
||||
<string name="signal_badge_ble_cd">Σάρωση BLE ενεργή</string>
|
||||
<string name="signal_badge_key_irk_cd">Η ταυτότητα επαληθεύτηκε</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Κρυπτογραφημένη σύνδεση</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Κεχριμπάρι</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Χρησιμοποιεί χρώματα ταπετσαρίας</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Παρακολούθηση</string>
|
||||
<string name="settings_category_appearance_label">Εμφάνιση</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Μη αποθηκευμένες αλλαγές</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Γενικά</string>
|
||||
<string name="device_settings_microphone_mode_label">Μικρόφωνο</string>
|
||||
<string name="device_settings_microphone_mode_description">Ποιο AirPod χρησιμοποιείται ως μικρόφωνο</string>
|
||||
<string name="device_settings_microphone_mode_description">Ποιο ακουστικό χρησιμοποιείται ως μικρόφωνο</string>
|
||||
<string name="device_settings_microphone_mode_auto">Αυτόματο</string>
|
||||
<string name="device_settings_microphone_mode_right">Δεξί</string>
|
||||
<string name="device_settings_microphone_mode_left">Αριστερό</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Ειδοποίηση φόρτισης</string>
|
||||
<string name="reaction_charged_notification_title">Η φόρτιση ολοκληρώθηκε</string>
|
||||
<string name="reaction_charged_notification_text_full">Το %1$s έχει φορτιστεί πλήρως.</string>
|
||||
<string name="reaction_charged_notification_text_partial">Το %1$s έχει φορτιστεί στο %2$d%%.</string>
|
||||
<string name="device_settings_rename_label">Μετονομασία</string>
|
||||
<string name="device_settings_rename_hint">Όνομα συσκευής</string>
|
||||
<string name="device_settings_rename_confirm">Μετονομασία</string>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Si compraste recientemente, puede demorar un momento para que Google Play sincronice.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Intentá de nuevo en unos minutos si tu compra no aparece.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Asegurate de haber iniciado sesión con la misma cuenta de Google usada para la compra.</string>
|
||||
<string name="settings_monitor_mode_label">Modo monitor</string>
|
||||
<string name="settings_monitor_mode_description">Bajo qué circunstancias esta app monitorea los datos de Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Notificación extra</string>
|
||||
<string name="settings_monitor_connected_notification_description">Muestra una notificación extra cuando un dispositivo está conectado. Esto te permite ocultar la notificación permanente \"Sin dispositivos\" desactivando el canal \"Estado del dispositivo\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Mantener la notificación después de la desconexión</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Pausa automática</string>
|
||||
<string name="settings_autopause_description">Pausar el audio al quitar el dispositivo de tu oreja.</string>
|
||||
<string name="settings_autopplay_label">Reproducción automática</string>
|
||||
<string name="settings_autoplay_description">Iniciar la reproducción de audio cuando se usa el dispositivo.</string>
|
||||
<string name="settings_autoplay_description">Reanuda la música al volver a ponerte los pods, pero solo si la pausa automática la detuvo. Las pausas que activaste vos mismo (teléfono, palanca de AirPods, suspensión) permanecen en pausa.</string>
|
||||
<string name="settings_start_music_on_wear_label">Iniciar música al ponérselos</string>
|
||||
<string name="settings_start_music_on_wear_description">Siempre intenta reproducir música cuando te ponés los pods, incluso después de una pausa manual.</string>
|
||||
<string name="settings_eardetection_info_label">Nota sobre la detección de oídos</string>
|
||||
<string name="settings_eardetection_info_description">Si la detección de auriculares solo funciona para un pod, se trata de una limitación de Apple. Solo se detecta el «pod principal» (utilizado para el micrófono). Configuración en dispositivos Apple: Ajustes → Bluetooth → AirPods → Micrófono.</string>
|
||||
<string name="settings_fake_data_label">Datos falsos</string>
|
||||
<string name="settings_fake_data_description">Mostrar datos falsos, es decir, simular dispositivos que no existen.</string>
|
||||
<string name="settings_debug_label">Ajustes de depuración</string>
|
||||
<string name="settings_debug_description">Ajustes adicionales para ayudar a solucionar problemas con la app.</string>
|
||||
<string name="settings_signal_minimum_label">Calidad de señal mínima</string>
|
||||
<string name="settings_signal_minimum_description">La calidad de señal mínima que un dispositivo necesita tener para ser considerado tuyo.</string>
|
||||
<string name="settings_autoconnect_label">Conexión automática</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Mostrar pop-up de conexión</string>
|
||||
<string name="settings_popup_connected_description">Mostrar un pop-up cuando el dispositivo se conecta por primera vez.</string>
|
||||
<string name="settings_popup_info_not_in_app">Los popups solo se muestran cuando no estás en la app.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Los popups requieren que el monitor esté en ejecución. El modo de monitor está configurado como \"Cuando la app está abierta\", lo que detiene el monitor cuando salís de la app.</string>
|
||||
<string name="general_fix_it_action">Corregirlo</string>
|
||||
<string name="settings_charged_notification_label">Notificación de carga completa</string>
|
||||
<string name="settings_charged_notification_description">Muestra una notificación cuando todo lo que se está cargando alcanza el nivel de carga objetivo.</string>
|
||||
<string name="settings_charged_threshold_label">Nivel de carga objetivo</string>
|
||||
<string name="settings_charged_scope_label">Baterías monitoreadas</string>
|
||||
<string name="settings_charged_scope_pods_label">Solo pods</string>
|
||||
<string name="settings_charged_scope_case_label">Solo estuche</string>
|
||||
<string name="settings_charged_scope_both_label">Pods y estuche</string>
|
||||
<string name="device_settings_experimental_title">Función experimental</string>
|
||||
<string name="device_settings_experimental_description">Esta función aún no se ha probado en todos los dispositivos. Si algo no funciona correctamente, por favor abrí un problema con un registro de depuración.</string>
|
||||
<string name="device_settings_experimental_action">Abrir seguimiento de problemas</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Depuración</string>
|
||||
<string name="settings_general_label">Ajustes</string>
|
||||
<string name="settings_general_description">Ajustes generales que afectan a toda la app.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Ocultar dispositivos no emparejados</string>
|
||||
<string name="settings_overview_hide_unmatched_description">No mostrar en el resumen los dispositivos cercanos que no coincidan con ninguno de tus perfiles, p. ej., los AirPods de otras personas.</string>
|
||||
<string name="settings_acknowledgements_label">Agradecimientos</string>
|
||||
<string name="settings_debug_autoreports_label">Reportes automáticos de errores</string>
|
||||
<string name="settings_debug_autoreports_description">Reporta problemas automáticamente, ej. detalles sobre un cierre inesperado de la app para que pueda averiguar cómo solucionarlo.</string>
|
||||
<string name="settings_debug_mode_label">Modo depuración</string>
|
||||
<string name="settings_debug_mode_description">Mostrar información adicional para solucionar problemas.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Datos BLE sin filtrar</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Eliminar cualquier filtro del escáner BLE para mostrar todos los datos BLE transmitidos. Útil para agregar soporte para nuevos tipos de auriculares.</string>
|
||||
<string name="settings_compatibility_mode_label">Modo de compatibilidad</string>
|
||||
<string name="settings_compatibility_mode_description">Desactivar optimizaciones para mejorar la compatibilidad. Probá esto si no estás viendo ningún dato.</string>
|
||||
<string name="help_translate_label">Traducción</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Este widget requiere una conexión directa (AAP) a tu dispositivo. Asegurate de que tu dispositivo esté emparejado y conectado en la app antes de usar este widget.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Este dispositivo no admite control de ruido</string>
|
||||
<string name="anc_widget_aap_not_connected_label">No conectado</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Cerca pero sin conexión</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">No está cerca</string>
|
||||
<string name="anc_widget_aap_connecting_label">Conectando…</string>
|
||||
<string name="tile_anc_label">Control de ruido</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Permiso requerido</string>
|
||||
<string name="tile_anc_subtitle_no_device">Sin dispositivo</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Sin control de ruido</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth desactivado</string>
|
||||
<string name="widget_config_screen_title">Configuración del widget</string>
|
||||
<string name="widget_configuration_title">Selecciona el dispositivo</string>
|
||||
<string name="widget_configuration_description">Elige qué perfil del dispositivo debe mostrar este complemento.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">El Bluetooth está desactivado, actívalo ;)</string>
|
||||
<string name="overview_monitoring_active_label">Monitoreo de dispositivos</string>
|
||||
<string name="overview_monitoring_active_description">Asegúrate de que tu dispositivo esté cerca y activo.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Conectado, pero sin datos</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Tu dispositivo está conectado, pero CAPod no está recibiendo datos en tiempo real. Tu teléfono puede necesitar una opción de compatibilidad: el solucionador de problemas puede intentar encontrar una automáticamente.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Ejecutar solucionador de problemas</string>
|
||||
<string name="overview_unmatched_devices_label">Dispositivos no emparejados</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d dispositivo sin perfil coincidente</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
|
||||
<string name="permission_system_alert_window_label">Ventana emergente</string>
|
||||
<string name="permission_system_alert_window_description">Permite que CAPod se superponga a otras aplicaciones para que la función «Mostrar ventana emergente».</string>
|
||||
<string name="settings_monitor_mode_manual_label">Cuando la aplicación está abierta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Cuando el dispositivo está conectado</string>
|
||||
<string name="settings_monitor_mode_always_label">Siempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Cuando está visible</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">El estuche está abierto</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">En el oído</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Transparencia</string>
|
||||
<string name="anc_mode_adaptive">Adaptivo</string>
|
||||
<string name="conversation_awareness_label">Conciencia de conversación</string>
|
||||
<string name="settings_conversation_action_label">Cuando empezás a hablar</string>
|
||||
<string name="settings_conversation_action_nothing_label">No hacer nada</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Bajar el volumen</string>
|
||||
<string name="settings_conversation_action_pause_label">Pausar el contenido</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Reducción de volumen</string>
|
||||
<string name="signal_badge_ble_cd">Escaneo BLE activo</string>
|
||||
<string name="signal_badge_key_irk_cd">Identidad verificada</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Conexión encriptada</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Ámbar</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Usa los colores del fondo de pantalla</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitoreo</string>
|
||||
<string name="settings_category_appearance_label">Apariencia</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Cambios sin guardar</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">General</string>
|
||||
<string name="device_settings_microphone_mode_label">Micrófono</string>
|
||||
<string name="device_settings_microphone_mode_description">Qué AirPod se usa como micrófono</string>
|
||||
<string name="device_settings_microphone_mode_description">Qué auricular se usa como micrófono</string>
|
||||
<string name="device_settings_microphone_mode_auto">Auto</string>
|
||||
<string name="device_settings_microphone_mode_right">Derecho</string>
|
||||
<string name="device_settings_microphone_mode_left">Izquierdo</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Notificación de carga completa</string>
|
||||
<string name="reaction_charged_notification_title">Carga completa</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s está completamente cargado.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s se ha cargado al %2$d%%.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Si has comprado recientemente, puede tardar un momento para que Google Play sincronice.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Intenta de nuevo en unos minutos si tu compra no aparece.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Asegúrate de haber iniciado sesión con la misma cuenta de Google utilizada para la compra.</string>
|
||||
<string name="settings_monitor_mode_label">Modo monitor</string>
|
||||
<string name="settings_monitor_mode_description">Bajo qué circunstancias esta aplicación monitorea los datos de Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Notificación adicional</string>
|
||||
<string name="settings_monitor_connected_notification_description">Muestra una notificación adicional cuando un dispositivo está conectado. Esto te permite ocultar la notificación permanente \"Sin dispositivos\" desactivando el canal \"Estado del dispositivo\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Mantener la notificación después de la desconexión</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Pausa automática</string>
|
||||
<string name="settings_autopause_description">Pausar el audio al quitar el dispositivo de tu oreja.</string>
|
||||
<string name="settings_autopplay_label">Reproducción automática</string>
|
||||
<string name="settings_autoplay_description">Iniciar la reproducción de audio cuando se usa el dispositivo.</string>
|
||||
<string name="settings_autoplay_description">Reanuda la música al volver a usar tus pods, pero solo si Auto pausa la detuvo. Las pausas que activaste tú mismo (teléfono, varilla de AirPods, suspensión) siguen en pausa.</string>
|
||||
<string name="settings_start_music_on_wear_label">Iniciar música al ponerse</string>
|
||||
<string name="settings_start_music_on_wear_description">Siempre intenta reproducir música cuando te pones los pods, incluso después de una pausa manual.</string>
|
||||
<string name="settings_eardetection_info_label">Nota sobre la detección de oídos</string>
|
||||
<string name="settings_eardetection_info_description">Si la detección de auriculares solo funciona para un pod, se trata de una limitación de Apple. Solo se detecta el «pod principal» (utilizado para el micrófono). Configuración en dispositivos Apple: Ajustes → Bluetooth → AirPods → Micrófono.</string>
|
||||
<string name="settings_fake_data_label">Datos falsos</string>
|
||||
<string name="settings_fake_data_description">Mostrar datos falsos, es decir, simular dispositivos que no existen.</string>
|
||||
<string name="settings_debug_label">Ajustes de depuración</string>
|
||||
<string name="settings_debug_description">Ajustes adicionales para ayudar a solucionar problemas con la aplicación.</string>
|
||||
<string name="settings_signal_minimum_label">Calidad de señal mínima</string>
|
||||
<string name="settings_signal_minimum_description">La calidad de señal mínima que un dispositivo necesita tener para ser considerado tuyo.</string>
|
||||
<string name="settings_autoconnect_label">Conexión automática</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Mostrar pop-up de conexión</string>
|
||||
<string name="settings_popup_connected_description">Mostrar un pop-up cuando el dispositivo se conecta por primera vez.</string>
|
||||
<string name="settings_popup_info_not_in_app">Las ventanas emergentes solo se muestran cuando no estás en la aplicación.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Las ventanas emergentes requieren que el monitor esté en ejecución. Tu modo de monitor está configurado en \"Cuando la aplicación está abierta\", lo que detiene el monitor cuando sales de la aplicación.</string>
|
||||
<string name="general_fix_it_action">Corregir</string>
|
||||
<string name="settings_charged_notification_label">Notificación de carga completa</string>
|
||||
<string name="settings_charged_notification_description">Muestra una notificación cuando todo lo que se está cargando alcanza el nivel de carga objetivo.</string>
|
||||
<string name="settings_charged_threshold_label">Nivel de carga objetivo</string>
|
||||
<string name="settings_charged_scope_label">Baterías monitoreadas</string>
|
||||
<string name="settings_charged_scope_pods_label">Solo audífonos</string>
|
||||
<string name="settings_charged_scope_case_label">Solo estuche</string>
|
||||
<string name="settings_charged_scope_both_label">Audífonos y estuche</string>
|
||||
<string name="device_settings_experimental_title">Función experimental</string>
|
||||
<string name="device_settings_experimental_description">Esta función aún no ha sido probada en todos los dispositivos. Si algo no funciona correctamente, por favor abre un problema con un registro de depuración.</string>
|
||||
<string name="device_settings_experimental_action">Abrir rastreador de problemas</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Depuración</string>
|
||||
<string name="settings_general_label">Ajustes</string>
|
||||
<string name="settings_general_description">Ajustes generales que afectan a toda la aplicación.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Ocultar dispositivos no emparejados</string>
|
||||
<string name="settings_overview_hide_unmatched_description">No mostrar dispositivos cercanos que no coincidan con ninguno de tus perfiles en el resumen, p. ej. los AirPods de otras personas.</string>
|
||||
<string name="settings_acknowledgements_label">Agradecimientos</string>
|
||||
<string name="settings_debug_autoreports_label">Reportes automáticos de errores</string>
|
||||
<string name="settings_debug_autoreports_description">Reporta problemas automáticamente, ej. detalles sobre un cierre inesperado de la aplicación para que pueda averiguar cómo solucionarlo.</string>
|
||||
<string name="settings_debug_mode_label">Modo depuración</string>
|
||||
<string name="settings_debug_mode_description">Mostrar información adicional para solucionar problemas.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Datos BLE sin filtrar</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Eliminar cualquier filtro del escáner BLE para mostrar todos los datos BLE transmitidos. Útil para agregar soporte para nuevos tipos de auriculares.</string>
|
||||
<string name="settings_compatibility_mode_label">Modo de compatibilidad</string>
|
||||
<string name="settings_compatibility_mode_description">Desactivar optimizaciones para mejorar la compatibilidad. Prueba esto si no estás viendo ningún dato.</string>
|
||||
<string name="help_translate_label">Traducción</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Este widget requiere una conexión directa (AAP) a tu dispositivo. Asegúrate de que tu dispositivo esté emparejado y conectado en la aplicación antes de usar este widget.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Este dispositivo no es compatible con el control de ruido</string>
|
||||
<string name="anc_widget_aap_not_connected_label">No conectado</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Cerca pero no conectado</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">No está cerca</string>
|
||||
<string name="anc_widget_aap_connecting_label">Conectando…</string>
|
||||
<string name="tile_anc_label">Control de ruido</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Permiso requerido</string>
|
||||
<string name="tile_anc_subtitle_no_device">Sin dispositivo</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Sin control de ruido</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth apagado</string>
|
||||
<string name="widget_config_screen_title">Configuración de widget</string>
|
||||
<string name="widget_configuration_title">Selecciona el dispositivo</string>
|
||||
<string name="widget_configuration_description">Elige qué perfil del dispositivo debe mostrar este complemento.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">El Bluetooth está desactivado, actívalo ;)</string>
|
||||
<string name="overview_monitoring_active_label">Monitoreo de dispositivos</string>
|
||||
<string name="overview_monitoring_active_description">Asegúrate de que tu dispositivo esté cerca y activo.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Conectado, pero sin datos</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Tu dispositivo está conectado, pero CAPod no está recibiendo datos en tiempo real. Es posible que tu teléfono necesite una opción de compatibilidad — el solucionador de problemas puede intentar encontrar una automáticamente.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Ejecutar solucionador de problemas</string>
|
||||
<string name="overview_unmatched_devices_label">Dispositivos no emparejados</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d dispositivo sin perfil coincidente</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
|
||||
<string name="permission_system_alert_window_label">Ventana emergente</string>
|
||||
<string name="permission_system_alert_window_description">Permite que CAPod se superponga a otras aplicaciones para que la función «Mostrar ventana emergente».</string>
|
||||
<string name="settings_monitor_mode_manual_label">Cuando la aplicación está abierta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Cuando el dispositivo está conectado</string>
|
||||
<string name="settings_monitor_mode_always_label">Siempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Cuando está visible</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">El estuche está abierto</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">En el oído</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Transparencia</string>
|
||||
<string name="anc_mode_adaptive">Adaptativo</string>
|
||||
<string name="conversation_awareness_label">Reconocimiento de conversación</string>
|
||||
<string name="settings_conversation_action_label">Cuando empiezas a hablar</string>
|
||||
<string name="settings_conversation_action_nothing_label">No hacer nada</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Bajar volumen</string>
|
||||
<string name="settings_conversation_action_pause_label">Pausar medios</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Reducción de volumen</string>
|
||||
<string name="signal_badge_ble_cd">Escaneo BLE activo</string>
|
||||
<string name="signal_badge_key_irk_cd">Identidad verificada</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Conexión cifrada</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Ámbar</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Usa los colores del fondo de pantalla</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitoreo</string>
|
||||
<string name="settings_category_appearance_label">Apariencia</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Cambios sin guardar</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">General</string>
|
||||
<string name="device_settings_microphone_mode_label">Micrófono</string>
|
||||
<string name="device_settings_microphone_mode_description">Qué AirPod se usa como micrófono</string>
|
||||
<string name="device_settings_microphone_mode_description">Qué audífono se usa como micrófono</string>
|
||||
<string name="device_settings_microphone_mode_auto">Automático</string>
|
||||
<string name="device_settings_microphone_mode_right">Derecho</string>
|
||||
<string name="device_settings_microphone_mode_left">Izquierdo</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Notificación de carga completa</string>
|
||||
<string name="reaction_charged_notification_title">Carga completada</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s está completamente cargado.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s se ha cargado al %2$d%%.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Si has comprado recientemente, puede tardar un momento para que Google Play sincronice.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Inténtalo de nuevo en unos minutos si tu compra no aparece.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Asegúrate de haber iniciado sesión con la misma cuenta de Google utilizada para la compra.</string>
|
||||
<string name="settings_monitor_mode_label">Modo monitor</string>
|
||||
<string name="settings_monitor_mode_description">Bajo qué circunstancias esta aplicación supervisa los datos de Bluetooth.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Notificación adicional</string>
|
||||
<string name="settings_monitor_connected_notification_description">Muestra una notificación adicional cuando un dispositivo está conectado. Esto le permite ocultar la notificación permanente \"Sin dispositivos\" deshabilitando el canal \"Estado del dispositivo\".</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Mantener la notificación después de la desconexión</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Pausa automática</string>
|
||||
<string name="settings_autopause_description">Pausar el audio al quitar el dispositivo de tu oído.</string>
|
||||
<string name="settings_autopplay_label">Reproducción automática</string>
|
||||
<string name="settings_autoplay_description">Iniciar la reproducción de audio cuando se lleva puesto el dispositivo.</string>
|
||||
<string name="settings_autoplay_description">Reanuda la música al volver a ponerte los auriculares, pero solo si fue Auto pausa quien la detuvo. Las pausas que hiciste tú (teléfono, varilla de los AirPods, suspensión) permanecen pausadas.</string>
|
||||
<string name="settings_start_music_on_wear_label">Iniciar música al ponérselos</string>
|
||||
<string name="settings_start_music_on_wear_description">Siempre intenta reproducir música cuando te pones los auriculares, incluso después de una pausa manual.</string>
|
||||
<string name="settings_eardetection_info_label">Nota sobre la detección de oídos</string>
|
||||
<string name="settings_eardetection_info_description">Si la detección de auriculares solo funciona para un pod, se trata de una limitación de Apple. Solo se detecta el «pod principal» (utilizado para el micrófono). Configuración en dispositivos Apple: Ajustes → Bluetooth → AirPods → Micrófono.</string>
|
||||
<string name="settings_fake_data_label">Datos falsos</string>
|
||||
<string name="settings_fake_data_description">Mostrar datos falsos, es decir, simular dispositivos que no existen.</string>
|
||||
<string name="settings_debug_label">Ajustes de depuración</string>
|
||||
<string name="settings_debug_description">Ajustes adicionales para ayudar a solucionar problemas con la aplicación.</string>
|
||||
<string name="settings_signal_minimum_label">Calidad mínima de la señal</string>
|
||||
<string name="settings_signal_minimum_description">La calidad mínima de la señal que debe tener un dispositivo para que se considere tuyo.</string>
|
||||
<string name="settings_autoconnect_label">Conexión automática</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Mostrar ventana emergente de conexión</string>
|
||||
<string name="settings_popup_connected_description">Mostrar una ventana emergente cuando el dispositivo se conecta por primera vez.</string>
|
||||
<string name="settings_popup_info_not_in_app">Los popups solo se muestran cuando no estás en la aplicación.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Los popups requieren que el monitor esté en ejecución. El modo de monitor está configurado en \"Cuando la app está abierta\", lo que detiene el monitor cuando sales de la aplicación.</string>
|
||||
<string name="general_fix_it_action">Corregirlo</string>
|
||||
<string name="settings_charged_notification_label">Notificación de carga completa</string>
|
||||
<string name="settings_charged_notification_description">Mostrar una notificación cuando todo lo que se está cargando alcance el nivel de carga objetivo.</string>
|
||||
<string name="settings_charged_threshold_label">Nivel de carga objetivo</string>
|
||||
<string name="settings_charged_scope_label">Baterías supervisadas</string>
|
||||
<string name="settings_charged_scope_pods_label">Solo auriculares</string>
|
||||
<string name="settings_charged_scope_case_label">Solo funda</string>
|
||||
<string name="settings_charged_scope_both_label">Auriculares y funda</string>
|
||||
<string name="device_settings_experimental_title">Función experimental</string>
|
||||
<string name="device_settings_experimental_description">Esta función aún no se ha probado en todos los dispositivos. Si algo no funciona correctamente, por favor abre una incidencia con un registro de depuración.</string>
|
||||
<string name="device_settings_experimental_action">Abrir rastreador de incidencias</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Depuración</string>
|
||||
<string name="settings_general_label">Ajustes</string>
|
||||
<string name="settings_general_description">Ajustes generales que afectan a toda la aplicación.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Ocultar dispositivos no reconocidos</string>
|
||||
<string name="settings_overview_hide_unmatched_description">No mostrar en el resumen los dispositivos cercanos que no coincidan con ninguno de tus perfiles, p. ej. los AirPods de otras personas.</string>
|
||||
<string name="settings_acknowledgements_label">Agradecimientos</string>
|
||||
<string name="settings_debug_autoreports_label">Informes de error automáticos</string>
|
||||
<string name="settings_debug_autoreports_description">Informa automáticamente de los problemas, p. ej., detalles sobre un fallo de la aplicación para que pueda averiguar cómo solucionarlo.</string>
|
||||
<string name="settings_debug_mode_label">Modo de depuración</string>
|
||||
<string name="settings_debug_mode_description">Mostrar información adicional para solucionar problemas.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Datos BLE sin filtrar</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Elimina cualquier filtro del escáner BLE para mostrar todos los datos BLE transmitidos. Útil para añadir compatibilidad con nuevos tipos de auriculares.</string>
|
||||
<string name="settings_compatibility_mode_label">Modo de compatibilidad</string>
|
||||
<string name="settings_compatibility_mode_description">Desactiva las optimizaciones para mejorar la compatibilidad. Prueba esto si no ves ningún dato.</string>
|
||||
<string name="help_translate_label">Traducción</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Este widget requiere una conexión directa (AAP) con tu dispositivo. Asegúrate de que tu dispositivo esté emparejado y conectado en la app antes de usar este widget.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Este dispositivo no admite control de ruido</string>
|
||||
<string name="anc_widget_aap_not_connected_label">No conectado</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Cerca pero no conectado</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">No cerca</string>
|
||||
<string name="anc_widget_aap_connecting_label">Conectando…</string>
|
||||
<string name="tile_anc_label">Control de ruido</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Permiso requerido</string>
|
||||
<string name="tile_anc_subtitle_no_device">Sin dispositivo</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Sin control de ruido</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth desactivado</string>
|
||||
<string name="widget_config_screen_title">Configuración del widget</string>
|
||||
<string name="widget_configuration_title">Selecciona el dispositivo</string>
|
||||
<string name="widget_configuration_description">Elige qué perfil del dispositivo debe mostrar este complemento.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">El Bluetooth está desactivado, actívalo ;)</string>
|
||||
<string name="overview_monitoring_active_label">Monitoreo de dispositivos</string>
|
||||
<string name="overview_monitoring_active_description">Asegúrate de que tu dispositivo esté cerca y activo.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Conectado, pero sin datos</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Tu dispositivo está conectado, pero CAPod no está recibiendo datos en directo. Es posible que tu teléfono necesite una opción de compatibilidad: el solucionador de problemas puede intentar encontrar una automáticamente.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Ejecutar solucionador de problemas</string>
|
||||
<string name="overview_unmatched_devices_label">Dispositivos no emparejados</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d dispositivo sin perfil coincidente</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
|
||||
<string name="permission_system_alert_window_label">Ventana emergente</string>
|
||||
<string name="permission_system_alert_window_description">Permite que CAPod se superponga a otras aplicaciones para que la función «Mostrar ventana emergente».</string>
|
||||
<string name="settings_monitor_mode_manual_label">Cuando la aplicación está abierta</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Cuando el dispositivo está conectado</string>
|
||||
<string name="settings_monitor_mode_always_label">Siempre</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Cuando está visible</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">El estuche está abierto</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">En el oído</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Transparencia</string>
|
||||
<string name="anc_mode_adaptive">Adaptativo</string>
|
||||
<string name="conversation_awareness_label">Conciencia de conversación</string>
|
||||
<string name="settings_conversation_action_label">Cuando empiezas a hablar</string>
|
||||
<string name="settings_conversation_action_nothing_label">No hacer nada</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Bajar volumen</string>
|
||||
<string name="settings_conversation_action_pause_label">Pausar multimedia</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Reducción de volumen</string>
|
||||
<string name="signal_badge_ble_cd">Escaneo BLE activo</string>
|
||||
<string name="signal_badge_key_irk_cd">Identidad verificada</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Conexión cifrada</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Ámbar</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Usa colores del fondo de pantalla</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitorización</string>
|
||||
<string name="settings_category_appearance_label">Apariencia</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Cambios sin guardar</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">General</string>
|
||||
<string name="device_settings_microphone_mode_label">Micrófono</string>
|
||||
<string name="device_settings_microphone_mode_description">Qué AirPod se usa como micrófono</string>
|
||||
<string name="device_settings_microphone_mode_description">Qué auricular se usa como micrófono</string>
|
||||
<string name="device_settings_microphone_mode_auto">Automático</string>
|
||||
<string name="device_settings_microphone_mode_right">Derecho</string>
|
||||
<string name="device_settings_microphone_mode_left">Izquierdo</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Notificación de carga completa</string>
|
||||
<string name="reaction_charged_notification_title">Carga completada</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s está completamente cargado.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s se ha cargado al %2$d%%.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Kui olete hiljuti ostnud, võib Google Playl kuluda ajakohastamiseks hetk.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Proovige mõne minuti pärast uuesti, kui teie ost ei ilmu.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Veenduge, et olete sisse logitud sama Google\'i kontoga, mida kasutasite ostu sooritamiseks.</string>
|
||||
<string name="settings_monitor_mode_label">Jälgimisrežiim</string>
|
||||
<string name="settings_monitor_mode_description">Mis olukordades jälgib see rakendus Bluetooth andmeid.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Lisateavitus</string>
|
||||
<string name="settings_monitor_connected_notification_description">Kuvab seadme ühendumisel lisateavituse. See peidab pidevalt nähtava „Seadmed puuduvad“ teate, selleks keelates „Seadme olek“ kanali.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Kuva teadet ka pärast eemaldamist</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Automaatne peatamine</string>
|
||||
<string name="settings_autopause_description">Seadme kõrvast eemaldamisel peata esitamine.</string>
|
||||
<string name="settings_autopplay_label">Automaatne esitamine</string>
|
||||
<string name="settings_autoplay_description">Automaatne esitamine, kui seade pannakse kõrva.</string>
|
||||
<string name="settings_autoplay_description">Jätka muusika kuulamist, kui kannad taas kõrvaklappe, kuid ainult siis, kui isekäivitunud paus selle peatas. Isepausimise (telefon, AirPodsi vars, uni) puhul jääb paus kestma.</string>
|
||||
<string name="settings_start_music_on_wear_label">Esita muusikat kõrva panemisel</string>
|
||||
<string name="settings_start_music_on_wear_description">Proovib alati esitada muusikat, kui paned kõrvaklapid pähe, isegi pärast enda käivitatud pausi.</string>
|
||||
<string name="settings_eardetection_info_label">Kõrva tuvastamisest</string>
|
||||
<string name="settings_eardetection_info_description">Kui tuvastatakse vaid üks klapp, siis seda põhjustab Apple\'i piirang. Tuvastatakse vaid peamine klapp (kasutatakse mikrofonina). Seadista Apple\'i seadmetes: Seaded (Settings) → Bluetooth → AirPods (AirPodid) → Mikrofon (Microphone).</string>
|
||||
<string name="settings_fake_data_label">Valeandmed</string>
|
||||
<string name="settings_fake_data_description">Näita valeandmeid, nt samad seadmed, mida pole olemas.</string>
|
||||
<string name="settings_debug_label">Vealogide seaded</string>
|
||||
<string name="settings_debug_description">Täiendavad seadmed rakenduse probleemide lahendamise aitamiseks.</string>
|
||||
<string name="settings_signal_minimum_label">Väikseim signaali kvaliteet</string>
|
||||
<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>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Näita ühenduse hüpikakent</string>
|
||||
<string name="settings_popup_connected_description">Näita seadme esmaühendumisel hüpikakent.</string>
|
||||
<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="settings_charged_notification_label">Laetud teatis</string>
|
||||
<string name="settings_charged_notification_description">Kuva teatis, kui kõik laadimise all olevad seadmed jõuavad sihitud laetuse tasemele.</string>
|
||||
<string name="settings_charged_threshold_label">Sihitud laetuse tase</string>
|
||||
<string name="settings_charged_scope_label">Jälgitavad akud</string>
|
||||
<string name="settings_charged_scope_pods_label">Ainult kõrvaklapid</string>
|
||||
<string name="settings_charged_scope_case_label">Ainult ümbris</string>
|
||||
<string name="settings_charged_scope_both_label">Kõrvaklapid ja ümbris</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>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Silumine</string>
|
||||
<string name="settings_general_label">Seaded</string>
|
||||
<string name="settings_general_description">Kogu rakendust mõjutavad üldseaded.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Peida mittevastavad seadmed</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Ära näita ülevaates lähedalasuvaid seadmeid, mis ei vasta ühelegi sinu profiilile, nt teiste inimeste AirPods.</string>
|
||||
<string name="settings_acknowledgements_label">Tunnustused</string>
|
||||
<string name="settings_debug_autoreports_label">Automaatsed veateated</string>
|
||||
<string name="settings_debug_autoreports_description">Teavitab vigadest ise, nt andmed rakenduse hangumise kohta, et saaksin mõista, kuidas olukorda lahendada.</string>
|
||||
<string name="settings_debug_mode_label">Veaotsing</string>
|
||||
<string name="settings_debug_mode_description">Näita vigade lahendamiseks täiendavat teavet.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Filtreerimata Bluetooth madalama energiatarbimisega andmed</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Eemalda kõik Bluetooth madalama energiatarbimisega skanneri filtrid, et näha kõiki edastatud Bluetooth madalama energiatarbimisega andmeid. Kasulik uute kõrvaklappide ühildamiseks.</string>
|
||||
<string name="settings_compatibility_mode_label">Ühilduvusrežiim</string>
|
||||
<string name="settings_compatibility_mode_description">Ühilduvuse parandamiseks keelake optimeeringud. Proovige seda, kui te ei näe mingeid andmeid.</string>
|
||||
<string name="help_translate_label">Tõlge</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">See vidin nõuab otsest ühendust (AAP) teie seadmega. Enne selle vidina kasutamist veenduge, et teie seade on rakenduses seotud ja ühendatud.</string>
|
||||
<string name="anc_widget_no_anc_support_label">See seade ei toeta mürakontrolli</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Ühendus puudub</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Lähedal, kuid pole ühendatud</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Pole lähedal</string>
|
||||
<string name="anc_widget_aap_connecting_label">Ühendamine…</string>
|
||||
<string name="tile_anc_label">Mürakontroll</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Vaja on luba</string>
|
||||
<string name="tile_anc_subtitle_no_device">Puudub seade</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Puudub mürakontroll</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth väljas</string>
|
||||
<string name="widget_config_screen_title">Vidina seadistus</string>
|
||||
<string name="widget_configuration_title">Vali seade</string>
|
||||
<string name="widget_configuration_description">Valige, millist seadmeprofiili see vidin võiks kuvada.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth on keelatud, lubage see ;)</string>
|
||||
<string name="overview_monitoring_active_label">Seadmete jälgimine</string>
|
||||
<string name="overview_monitoring_active_description">Veenduge, et seade oleks läheduses ja kasutusel.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Ühendatud, kuid andmeid pole</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Seade on ühendatud, kuid CAPod ei saa sellelt otseandmeid. Telefon vajab tõenäoliselt ühilduvuse valikut — trikijär kontrollib, kas suudab selle automaatselt leida.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Käivita trikijär</string>
|
||||
<string name="overview_unmatched_devices_label">Omavahel sobimatud seadmed</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d seade ei kattu profiiliga</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Vaja on järgmist luba:</string>
|
||||
<string name="permission_system_alert_window_label">Süsteemi teate aken</string>
|
||||
<string name="permission_system_alert_window_description">Luba CAPodil toimetada teiste rakenduste kohal, et näha hüpikaknaid.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Kui rakendus on avatud</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Kui seade on ühendatud</string>
|
||||
<string name="settings_monitor_mode_always_label">Alati</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Kui nähtaval</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Kaas on avatud</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Kõrvas</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Läbipaistvus</string>
|
||||
<string name="anc_mode_adaptive">Kohanduv</string>
|
||||
<string name="conversation_awareness_label">Vestluse tuvastamine</string>
|
||||
<string name="settings_conversation_action_label">Kui alustad rääkimist</string>
|
||||
<string name="settings_conversation_action_nothing_label">Ära tee midagi</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Vähenda helitugevust</string>
|
||||
<string name="settings_conversation_action_pause_label">Peata meedia</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Helitugevuse vähendamine</string>
|
||||
<string name="signal_badge_ble_cd">BLE skannimine aktiivne</string>
|
||||
<string name="signal_badge_key_irk_cd">Identiteet kinnitatud</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Krüpteeritud ühendus</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Merevaik</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Kasutab taustapildi värve</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Jälgimine</string>
|
||||
<string name="settings_category_appearance_label">Välimus</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Salvestamata muudatused</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Üldine</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofon</string>
|
||||
<string name="device_settings_microphone_mode_description">Millist AirPodi kasutatakse mikrofonina</string>
|
||||
<string name="device_settings_microphone_mode_description">Kumba kõrvaklappi kasutatakse mikrofonina</string>
|
||||
<string name="device_settings_microphone_mode_auto">Automaatne</string>
|
||||
<string name="device_settings_microphone_mode_right">Parem</string>
|
||||
<string name="device_settings_microphone_mode_left">Vasak</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Laetud teatis</string>
|
||||
<string name="reaction_charged_notification_title">Laadimine lõpetatud</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s on täielikult laetud.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s on laetud %2$d%%-ni.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">Duela gutxi erosi baduzu, baliteke Google Play-k sinkronizatzeko une bat behar izatea.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">Saiatu berriro minutu batzuen buruan zure erosketa agertzen ez bada.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">Ziurtatu erosketan erabilitako Google kontu berarekin saioa hasita daudela.</string>
|
||||
<string name="settings_monitor_mode_label">Monitorizazio modua</string>
|
||||
<string name="settings_monitor_mode_description">Zein egoeratan monitorizatzen ditu aplikazio honek Bluetooth datuak.</string>
|
||||
<string name="settings_monitor_connected_notification_label">Jakinarazpen gehigarria</string>
|
||||
<string name="settings_monitor_connected_notification_description">Gailu bat konektatuta dagoenean jakinarazpen gehigarri bat erakusten du. Honek \"Gailurik ez\" jakinarazpen iraunkorra ezkutatzeko aukera ematen dizu \"Gailuaren egoera\" kanala desgaituz.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Mantendu jakinarazpena deskonektatu ondoren</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">Pausatze automatikoa</string>
|
||||
<string name="settings_autopause_description">Pausatu audioa gailua belarritik kentzean.</string>
|
||||
<string name="settings_autopplay_label">Erreprodukzio automatikoa</string>
|
||||
<string name="settings_autoplay_description">Hasi audio erreprodukzioa gailua jantzita dagoenean.</string>
|
||||
<string name="settings_autoplay_description">Berriro jantzita musika errepikatzen du, baina soilik Eten automatikoak gelditu bazuen. Zuk eragindako etenak (telefonoa, AirPods beso, loa) etenda geratzen dira.</string>
|
||||
<string name="settings_start_music_on_wear_label">Hasi musika jantzitakoan</string>
|
||||
<string name="settings_start_music_on_wear_description">Beti saiatzen da musika erreproduzitzen aurrikulariak jartzen dituzunean, eskuzko eten baten ondoren ere.</string>
|
||||
<string name="settings_eardetection_info_label">Belarri-detekzioaren oharra</string>
|
||||
<string name="settings_eardetection_info_description">Belarri-detekzioak pod batentzat soilik funtzionatzen badu, Apple-ren muga bat da. \"Pod nagusia\" (mikrofonorako erabiltzen dena) soilik detektatzen da. Konfiguratu Apple gailuetan: Ezarpenak → Bluetooth → AirPods → Mikrofonoa.</string>
|
||||
<string name="settings_fake_data_label">Datu faltsuak</string>
|
||||
<string name="settings_fake_data_description">Erakutsi datu faltsuak, hau da, existitzen ez diren gailuak simulatu.</string>
|
||||
<string name="settings_debug_label">Arazketa ezarpenak</string>
|
||||
<string name="settings_debug_description">Aplikazioarekin arazoak konpontzen laguntzeko ezarpen gehigarriak.</string>
|
||||
<string name="settings_signal_minimum_label">Seinalearen gutxieneko kalitatea</string>
|
||||
<string name="settings_signal_minimum_description">Gailu batek zurea dela kontsideratzeko izan behar duen seinalearen gutxieneko kalitatea.</string>
|
||||
<string name="settings_autoconnect_label">Automatikoki konektatu</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">Erakutsi konexioaren pop-up-a</string>
|
||||
<string name="settings_popup_connected_description">Erakutsi pop-up bat gailua lehen aldiz konektatzen denean.</string>
|
||||
<string name="settings_popup_info_not_in_app">Leiho gainerakorrak soilik erakusten dira aplikazioan ez zaudenerako.</string>
|
||||
<string name="settings_popup_warning_manual_mode">Leiho gainerakorrek monitorea martxan egotea behar dute. Zure monitor modua \"Aplikazioa irekita dagoenean\" ezarrita dago, eta horrek monitorea gelditzen du aplikaziotik irteten zarenean.</string>
|
||||
<string name="general_fix_it_action">Konpondu</string>
|
||||
<string name="settings_charged_notification_label">Kargatutako jakinarazpena</string>
|
||||
<string name="settings_charged_notification_description">Erakutsi jakinarazpen bat kargatzen ari den guztia helburuko karga-maila lortzen duenean.</string>
|
||||
<string name="settings_charged_threshold_label">Helburuko karga-maila</string>
|
||||
<string name="settings_charged_scope_label">Monitorizatutako bateriak</string>
|
||||
<string name="settings_charged_scope_pods_label">Podak soilik</string>
|
||||
<string name="settings_charged_scope_case_label">Kaxa soilik</string>
|
||||
<string name="settings_charged_scope_both_label">Podak eta kaxa</string>
|
||||
<string name="device_settings_experimental_title">Funtzio esperimentala</string>
|
||||
<string name="device_settings_experimental_description">Funtzio hau oraindik ez da gailu guztietan probatu. Zerbait ondo funtzionatzen ez badu, mesedez ireki arazo bat arazketa-erregistroarekin.</string>
|
||||
<string name="device_settings_experimental_action">Ireki arazo-jarraipena</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">Arazketa</string>
|
||||
<string name="settings_general_label">Ezarpenak</string>
|
||||
<string name="settings_general_description">Aplikazio osoari eragiten dioten doikuntza orokorrak.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">Ezkutatu bat ez datozen gailuak</string>
|
||||
<string name="settings_overview_hide_unmatched_description">Ez erakutsi ikuspegiaren inguruan dauden eta zure profilekin bat ez datozen gailuak, esaterako, beste pertsonen AirPods-ak.</string>
|
||||
<string name="settings_acknowledgements_label">Eskertzak</string>
|
||||
<string name="settings_debug_autoreports_label">Erroreen txosten automatikoak</string>
|
||||
<string name="settings_debug_autoreports_description">Automatikoki arazoak salatzen ditu, adib. aplikazioaren kraskaduraren xehetasunak, konpontzen jakin dezadan.</string>
|
||||
<string name="settings_debug_mode_label">Arazketa modua</string>
|
||||
<string name="settings_debug_mode_description">Erakutsi informazio gehigarria arazoak konpontzeko.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">Iragazi gabeko BLE datuak</string>
|
||||
<string name="settings_blescanner_unfiltered_description">Kendu BLE eskanerreko iragazki guztiak igorritako BLE datu guztiak erakusteko. Entzungailu mota berrietarako euskarria gehitzeko erabilgarria.</string>
|
||||
<string name="settings_compatibility_mode_label">Bateragarritasun modua</string>
|
||||
<string name="settings_compatibility_mode_description">Desgaitu optimizazioak bateragarritasuna hobetzeko. Saiatu hau daturik ikusten ez baduzu.</string>
|
||||
<string name="help_translate_label">Itzulpena</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">Widget honek zure gailuarekin konexio zuzen bat (AAP) behar du. Ziurtatu zure gailua parekatuta eta konektatuta dagoela aplikazioan widget hau erabili aurretik.</string>
|
||||
<string name="anc_widget_no_anc_support_label">Gailu honek ez ditu onartzen zarata-kontrolaren funtzioak</string>
|
||||
<string name="anc_widget_aap_not_connected_label">Konektatu gabe</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">Gertu baina konektatu gabe</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">Ez dago gertu</string>
|
||||
<string name="anc_widget_aap_connecting_label">Konektatzen…</string>
|
||||
<string name="tile_anc_label">Zarata kontrola</string>
|
||||
<string name="tile_anc_subtitle_permission_required">Baimena beharrezkoa da</string>
|
||||
<string name="tile_anc_subtitle_no_device">Gailurik ez</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">Ez dago zarata kontrolik</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">Bluetooth itzalita</string>
|
||||
<string name="widget_config_screen_title">Widget konfigurazioa</string>
|
||||
<string name="widget_configuration_title">Hautatu Gailua</string>
|
||||
<string name="widget_configuration_description">Aukeratu trepeta honek zein gailu-profil erakutsi behar duen.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth-a desgaituta dago, gaitu ezazu ;)</string>
|
||||
<string name="overview_monitoring_active_label">Gailuen jarraipena</string>
|
||||
<string name="overview_monitoring_active_description">Ziurtatu zure gailua gertuan eta aktibo dagoela.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">Konektatuta, baina daturik ez</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">Zure gailua konektatuta dago, baina CAPod-ek ez du zuzeneko daturik jasotzen. Baliteke zure telefonoak bateragarritasun-aukera bat behar izatea — arazo-konpontzaileak automatikoki bila dezake.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">Exekutatu arazo-konpontzailea</string>
|
||||
<string name="overview_unmatched_devices_label">Bat ez datozen gailuak</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d gailu bat datorren profilik gabe</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">Hurrengo baimena beharrezkoa da:</string>
|
||||
<string name="permission_system_alert_window_label">Sistemaren alerta-leihoa</string>
|
||||
<string name="permission_system_alert_window_description">Baimendu CAPod-i beste aplikazioen gainean marraztea \"Erakutsi pop-up-a\" ezaugarria posible egiteko.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Aplikazioa irekita dagoenean</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Gailua konektatuta dagoenean</string>
|
||||
<string name="settings_monitor_mode_always_label">Beti</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Ikusten denean</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Kaxa irekita dago</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">Belarrian</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">Gardentasuna</string>
|
||||
<string name="anc_mode_adaptive">Egokigarria</string>
|
||||
<string name="conversation_awareness_label">Elkarrizketa-kontzientzia</string>
|
||||
<string name="settings_conversation_action_label">Hitz egiten hasten zarenean</string>
|
||||
<string name="settings_conversation_action_nothing_label">Ez egin ezer</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">Jaitsi bolumena</string>
|
||||
<string name="settings_conversation_action_pause_label">Pausatu multimedia</string>
|
||||
<string name="settings_conversation_volume_reduction_label">Bolumen murrizketa</string>
|
||||
<string name="signal_badge_ble_cd">BLE eskaneatzea aktibo</string>
|
||||
<string name="signal_badge_key_irk_cd">Identitatea egiaztatuta</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Enkriptatutako konexioa</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">Anbarra</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">Horma-papereko koloreak erabiltzen ditu</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">Monitorizazioa</string>
|
||||
<string name="settings_category_appearance_label">Itxura</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Gorde gabeko aldaketak</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">Orokorra</string>
|
||||
<string name="device_settings_microphone_mode_label">Mikrofonoa</string>
|
||||
<string name="device_settings_microphone_mode_description">Zein AirPod erabiltzen den mikrofono gisa</string>
|
||||
<string name="device_settings_microphone_mode_description">Zein entzungailu erabiltzen den mikrofono gisa</string>
|
||||
<string name="device_settings_microphone_mode_auto">Automatikoa</string>
|
||||
<string name="device_settings_microphone_mode_right">Eskuinekoa</string>
|
||||
<string name="device_settings_microphone_mode_left">Ezkerrekoa</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">Kargatutako jakinarazpena</string>
|
||||
<string name="reaction_charged_notification_title">Karga osatu da</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s guztiz kargatuta dago.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s %2$d%%-era kargatu da.</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>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<string name="upgrade_screen_restore_troubleshooting_msg">اگر اخیراً خرید کردهاید، ممکن است چند لحظه طول بکشد تا Google Play همگامسازی شود.</string>
|
||||
<string name="upgrade_screen_restore_sync_patience_hint">اگر خرید شما نمایش داده نشد، چند دقیقه دیگر دوباره امتحان کنید.</string>
|
||||
<string name="upgrade_screen_restore_multiaccount_hint">مطمئن شوید با همان حساب Google که برای خرید استفاده کردید وارد شدهاید.</string>
|
||||
<string name="settings_monitor_mode_label">حالت نظارت</string>
|
||||
<string name="settings_monitor_mode_description">این برنامه تحت چه شرایطی دادههای بلوتوث را نظارت میکند.</string>
|
||||
<string name="settings_monitor_connected_notification_label">اعلان اضافی</string>
|
||||
<string name="settings_monitor_connected_notification_description">هنگامی که یک دستگاه متصل است، یک اعلان اضافی نشان میدهد. این به شما امکان میدهد اعلان دائمی \"بدون دستگاه\" را با غیرفعال کردن کانال \"وضعیت دستگاه\" پنهان کنید.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">حفظ اعلان پس از قطع اتصال</string>
|
||||
@@ -49,13 +47,11 @@
|
||||
<string name="settings_autopause_label">مکث خودکار</string>
|
||||
<string name="settings_autopause_description">هنگام برداشتن دستگاه از گوش، صدا را متوقف کنید.</string>
|
||||
<string name="settings_autopplay_label">پخش خودکار</string>
|
||||
<string name="settings_autoplay_description">هنگام استفاده از دستگاه، پخش صدا را شروع کنید.</string>
|
||||
<string name="settings_autoplay_description">موسیقی را هنگام پوشیدن مجدد پادها از سر میگیرد، اما تنها اگر توقف خودکار آن را متوقف کرده باشد. توقفهایی که خودتان ایجاد کردید (تلفن، دکمه AirPods، خواب) همچنان متوقف میمانند.</string>
|
||||
<string name="settings_start_music_on_wear_label">شروع موسیقی هنگام پوشیدن</string>
|
||||
<string name="settings_start_music_on_wear_description">همیشه سعی میکند هنگام گذاشتن پادها موسیقی پخش کند، حتی پس از توقف دستی.</string>
|
||||
<string name="settings_eardetection_info_label">توجه درباره تشخیص گوش</string>
|
||||
<string name="settings_eardetection_info_description">اگر تشخیص گوش فقط برای یک پاد کار میکند، این محدودیت Apple است. فقط \"پاد اصلی\" (که برای میکروفون استفاده میشود) تشخیص داده میشود. در دستگاههای Apple تنظیم کنید: تنظیمات → بلوتوث → AirPods → میکروفون.</string>
|
||||
<string name="settings_fake_data_label">دادههای جعلی</string>
|
||||
<string name="settings_fake_data_description">دادههای جعلی را نشان دهید، یعنی دستگاههایی را که وجود ندارند شبیهسازی کنید.</string>
|
||||
<string name="settings_debug_label">تنظیمات اشکالزدایی</string>
|
||||
<string name="settings_debug_description">تنظیمات اضافی برای کمک به عیبیابی مشکلات برنامه.</string>
|
||||
<string name="settings_signal_minimum_label">حداقل کیفیت سیگنال</string>
|
||||
<string name="settings_signal_minimum_description">حداقل کیفیت سیگنالی که یک دستگاه باید داشته باشد تا متعلق به شما در نظر گرفته شود.</string>
|
||||
<string name="settings_autoconnect_label">اتصال خودکار</string>
|
||||
@@ -80,8 +76,13 @@
|
||||
<string name="settings_popup_connected_label">نمایش پاپآپ اتصال</string>
|
||||
<string name="settings_popup_connected_description">هنگام اتصال دستگاه برای اولین بار، یک پاپآپ نشان داده شود.</string>
|
||||
<string name="settings_popup_info_not_in_app">پنجرههای بازشو فقط زمانی نشان داده میشوند که در برنامه نباشید.</string>
|
||||
<string name="settings_popup_warning_manual_mode">پنجرههای بازشو نیاز دارند که مانیتور در حال اجرا باشد. حالت مانیتور شما روی «وقتی برنامه باز است» تنظیم شده که هنگام خروج از برنامه، مانیتور را متوقف میکند.</string>
|
||||
<string name="general_fix_it_action">رفع مشکل</string>
|
||||
<string name="settings_charged_notification_label">اعلان شارژ کامل</string>
|
||||
<string name="settings_charged_notification_description">پس از اینکه همه چیزهایی که در حال شارژ هستند به سطح شارژ هدف رسیدند، یک اعلان نمایش داده میشود.</string>
|
||||
<string name="settings_charged_threshold_label">سطح شارژ هدف</string>
|
||||
<string name="settings_charged_scope_label">باتریهای تحت نظارت</string>
|
||||
<string name="settings_charged_scope_pods_label">فقط پادها</string>
|
||||
<string name="settings_charged_scope_case_label">فقط کیس</string>
|
||||
<string name="settings_charged_scope_both_label">پادها و کیس</string>
|
||||
<string name="device_settings_experimental_title">ویژگی آزمایشی</string>
|
||||
<string name="device_settings_experimental_description">این ویژگی هنوز روی همه دستگاهها آزمایش نشده است. اگر چیزی درست کار نمیکند، لطفاً یک گزارش خطا با لاگ دیباگ باز کنید.</string>
|
||||
<string name="device_settings_experimental_action">باز کردن ردیاب مشکلات</string>
|
||||
@@ -119,13 +120,11 @@
|
||||
<string name="settings_category_debug_label">اشکالزدایی</string>
|
||||
<string name="settings_general_label">تنظیمات</string>
|
||||
<string name="settings_general_description">تنظیمات کلی که بر کل برنامه تأثیر میگذارند.</string>
|
||||
<string name="settings_overview_hide_unmatched_label">پنهان کردن دستگاههای نامتناسب</string>
|
||||
<string name="settings_overview_hide_unmatched_description">دستگاههای نزدیکی که با هیچکدام از پروفایلهای شما تطابق ندارند در نمای کلی نمایش داده نمیشوند، مثلاً AirPods دیگران.</string>
|
||||
<string name="settings_acknowledgements_label">قدردانیها</string>
|
||||
<string name="settings_debug_autoreports_label">گزارشهای خطای خودکار</string>
|
||||
<string name="settings_debug_autoreports_description">مشکلات را بهطور خودکار گزارش میدهد، مثلاً جزئیات مربوط به خرابی برنامه تا بتوانم نحوه رفع آن را بفهمم.</string>
|
||||
<string name="settings_debug_mode_label">حالت اشکالزدایی</string>
|
||||
<string name="settings_debug_mode_description">نمایش اطلاعات اضافی برای عیبیابی مشکلات.</string>
|
||||
<string name="settings_blescanner_unfiltered_label">دادههای BLE فیلترنشده</string>
|
||||
<string name="settings_blescanner_unfiltered_description">هرگونه فیلتر را از اسکنر BLE حذف کنید تا تمام دادههای BLE پخششده نمایش داده شود. برای افزودن پشتیبانی از انواع جدید هدفون مفید است.</string>
|
||||
<string name="settings_compatibility_mode_label">حالت سازگاری</string>
|
||||
<string name="settings_compatibility_mode_description">بهینهسازیها را برای بهبود سازگاری غیرفعال کنید. اگر هیچ دادهای نمیبینید، این را امتحان کنید.</string>
|
||||
<string name="help_translate_label">ترجمه</string>
|
||||
@@ -137,7 +136,14 @@
|
||||
<string name="anc_widget_config_aap_required_hint">این ویجت نیاز به اتصال مستقیم (AAP) به دستگاه شما دارد. قبل از استفاده از این ویجت، مطمئن شوید دستگاه شما در برنامه جفتشده و متصل است.</string>
|
||||
<string name="anc_widget_no_anc_support_label">این دستگاه از کنترل نویز پشتیبانی نمیکند</string>
|
||||
<string name="anc_widget_aap_not_connected_label">متصل نشده</string>
|
||||
<string name="anc_widget_aap_not_connected_nearby_description">در مجاورت اما متصل نیست</string>
|
||||
<string name="anc_widget_aap_not_connected_not_nearby_description">در مجاورت نیست</string>
|
||||
<string name="anc_widget_aap_connecting_label">در حال اتصال…</string>
|
||||
<string name="tile_anc_label">کنترل نویز</string>
|
||||
<string name="tile_anc_subtitle_permission_required">مجوز لازم است</string>
|
||||
<string name="tile_anc_subtitle_no_device">بدون دستگاه</string>
|
||||
<string name="tile_anc_subtitle_no_anc_support">بدون کنترل نویز</string>
|
||||
<string name="tile_anc_subtitle_bluetooth_off">بلوتوث خاموش است</string>
|
||||
<string name="widget_config_screen_title">پیکربندی ابزارک</string>
|
||||
<string name="widget_configuration_title">انتخاب دستگاه</string>
|
||||
<string name="widget_configuration_description">انتخاب کنید این ویجت کدام پروفایل دستگاه را نمایش دهد.</string>
|
||||
@@ -196,6 +202,9 @@
|
||||
<string name="overview_bluetooth_disabled_description">بلوتوث غیرفعال است، آن را فعال کنید ;)</string>
|
||||
<string name="overview_monitoring_active_label">نظارت بر دستگاهها</string>
|
||||
<string name="overview_monitoring_active_description">مطمئن شوید دستگاه شما نزدیک و فعال است.</string>
|
||||
<string name="overview_troubleshoot_suggestion_label">متصل است، اما دادهای وجود ندارد</string>
|
||||
<string name="overview_troubleshoot_suggestion_description">دستگاه شما متصل است، اما CAPod هیچ داده زندهای از آن دریافت نمیکند. ممکن است تلفن شما به یک گزینه سازگاری نیاز داشته باشد — عیبیاب میتواند بهطور خودکار تلاش کند آن را پیدا کند.</string>
|
||||
<string name="overview_troubleshoot_suggestion_action">اجرای عیبیاب</string>
|
||||
<string name="overview_unmatched_devices_label">دستگاههای بدون تطابق</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d دستگاه بدون پروفایل منطبق</item>
|
||||
@@ -220,9 +229,6 @@
|
||||
<string name="permission_required_title">مجوز زیر مورد نیاز است:</string>
|
||||
<string name="permission_system_alert_window_label">پنجره هشدار سیستم</string>
|
||||
<string name="permission_system_alert_window_description">به CAPod اجازه دهید روی سایر برنامهها نمایش دهد تا ویژگی \"نمایش پاپآپ\" ممکن شود.</string>
|
||||
<string name="settings_monitor_mode_manual_label">وقتی برنامه باز است</string>
|
||||
<string name="settings_monitor_mode_automatic_label">وقتی دستگاه متصل است</string>
|
||||
<string name="settings_monitor_mode_always_label">همیشه</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">وقتی دیده شود</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">کیس باز است</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">در گوش</string>
|
||||
@@ -254,6 +260,11 @@
|
||||
<string name="anc_mode_transparency">شفافیت</string>
|
||||
<string name="anc_mode_adaptive">تطبیقی</string>
|
||||
<string name="conversation_awareness_label">آگاهی مکالمه</string>
|
||||
<string name="settings_conversation_action_label">وقتی شروع به صحبت میکنید</string>
|
||||
<string name="settings_conversation_action_nothing_label">هیچ کاری نکن</string>
|
||||
<string name="settings_conversation_action_lower_volume_label">کاهش صدا</string>
|
||||
<string name="settings_conversation_action_pause_label">توقف رسانه</string>
|
||||
<string name="settings_conversation_volume_reduction_label">میزان کاهش صدا</string>
|
||||
<string name="signal_badge_ble_cd">اسکن BLE فعال است</string>
|
||||
<string name="signal_badge_key_irk_cd">هویت تأیید شد</string>
|
||||
<string name="signal_badge_key_encrypted_cd">اتصال رمزگذاریشده</string>
|
||||
@@ -322,7 +333,6 @@
|
||||
<string name="ui_theme_color_amber_label">کهربی</string>
|
||||
<string name="ui_theme_color_disabled_materialyou">از رنگهای تصویر پسزمینه استفاده میکند</string>
|
||||
<!-- Settings categories -->
|
||||
<string name="settings_category_monitoring_label">پایش</string>
|
||||
<string name="settings_category_appearance_label">ظاهر</string>
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">تغییرات ذخیرهنشده</string>
|
||||
@@ -470,7 +480,7 @@
|
||||
<!-- New device settings -->
|
||||
<string name="device_settings_category_general_label">عمومی</string>
|
||||
<string name="device_settings_microphone_mode_label">میکروفون</string>
|
||||
<string name="device_settings_microphone_mode_description">کدام AirPod به عنوان میکروفون استفاده میشود</string>
|
||||
<string name="device_settings_microphone_mode_description">کدام ایرباد به عنوان میکروفون استفاده میشود</string>
|
||||
<string name="device_settings_microphone_mode_auto">خودکار</string>
|
||||
<string name="device_settings_microphone_mode_right">راست</string>
|
||||
<string name="device_settings_microphone_mode_left">چپ</string>
|
||||
@@ -492,6 +502,10 @@
|
||||
<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="reaction_charged_channel_label">اعلان شارژ کامل</string>
|
||||
<string name="reaction_charged_notification_title">شارژ کامل شد</string>
|
||||
<string name="reaction_charged_notification_text_full">%1$s کاملاً شارژ شده است.</string>
|
||||
<string name="reaction_charged_notification_text_partial">%1$s تا %2$d%% شارژ شده است.</string>
|
||||
<string name="device_settings_rename_label">تغییر نام</string>
|
||||
<string name="device_settings_rename_hint">نام دستگاه</string>
|
||||
<string name="device_settings_rename_confirm">تغییر نام</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user