Compare commits

...
54 Commits
Author SHA1 Message Date
darken fb8ec72825 Release: 2.9.3-rc0 2023-06-27 22:07:22 +02:00
Matthias Urhahn d1687f574f Merge pull request #145 from d4rken-org/bump_workmanager
Fix background execution errors.
2023-06-27 22:04:28 +02:00
darken 72f47bec1f Fix background execution errors.
Bump workmanager `2.7.1` -> `2.8.1` to fix

```java
android.app.ForegroundServiceStartNotAllowedException: startForegroundService() not allowed due to mAllowStartForeground false: service eu.darken.capod/androidx.work.impl.foreground.SystemForegroundService
```
2023-06-27 21:55:58 +02:00
Matthias Urhahn 20e8aecf83 Merge pull request #144 from d4rken-org/gplay_hates_splash
Fix "Missing app icon in splash screen"
2023-06-27 21:49:50 +02:00
Matthias Urhahn ee41474670 Merge pull request #143 from d4rken-org/gplay_wear_policies_fml
Fix "Missing show time"
2023-06-27 21:49:37 +02:00
darken ac29e28eda Fix "Missing app icon in splash screen"
> Your app does not show a 48x48dp app icon on a black background during app startup. For more information, see Branded launch.
2023-06-27 21:49:05 +02:00
darken 58b96cbdee Fix "Missing show time"
> Your app does not display the time of day clearly at the top of the app home screen and any ongoing activity screens. We recommend that you display the time of day at the top of all activities except dialog and confirmation screens. For more information, see Show the time.
2023-06-27 21:12:16 +02:00
darken 0c1fc15251 Update translations 2023-06-27 20:28:31 +02:00
darken 7b8e0cc952 Update translations 2023-06-27 20:28:31 +02:00
darken c8aa591feb Update AGP 2023-06-27 20:28:21 +02:00
darken afa4d6e807 Release: 2.9.2-rc0 2023-05-16 09:23:59 +02:00
darken e6b7eda9e7 Update store translations 2023-05-16 09:23:09 +02:00
darken bbc4e05c8f Update translations 2023-05-16 09:23:09 +02:00
darken 50bccbaa2e Update Crowdin tooling 2023-05-16 09:23:09 +02:00
Matthias Urhahn d71e83a2af Merge pull request #132 from d4rken-org/tweak_widget_layout
Tweak widget layout
2023-05-16 09:14:30 +02:00
Matthias Urhahn 92a67702b7 Merge pull request #131 from d4rken-org/gradle_bump
Gradle bump
2023-05-16 09:04:46 +02:00
darken 2f9185efa7 Tweak dual pod widget.
Less padding to reduce cutoff chances and put the label at the type bottom as it's the least important info.
2023-05-16 09:00:58 +02:00
darken a10216520b Fix resource reference 2023-05-16 08:59:54 +02:00
darken d5354e5156 Fix resource reference 2023-05-16 08:55:35 +02:00
darken a8f9b1f732 Use non final resource ids 2023-05-16 08:07:48 +02:00
darken 08a796e621 Fix wear namespace 2023-05-16 08:05:59 +02:00
darken 15f3a68f8e Use non transitive R classes to improve performance 2023-05-16 07:58:28 +02:00
darken e0bc6dafa0 Bump gradle 2023-05-16 07:46:13 +02:00
darken 430c1236d7 Fix typo 2023-05-07 20:15:41 +02:00
darken 3ac357161a Improve strings based on translator feedback 2023-05-07 20:15:41 +02:00
darken 3c5f1bf21f CI: Remove unused base flavor, all our modules are flavored :) 2023-04-07 11:00:44 +02:00
darken 5544cd3770 CI: Update action dependencies, improve naming and use more specific build commands 2023-04-07 11:00:44 +02:00
darken 71a060feb9 Release: 2.9.1-rc0 2023-04-05 16:19:46 +02:00
darken 25d643257c Update translations 2023-04-05 13:13:42 +02:00
darken 85900e5bff Improve final headphones checks and fix IllegalStateException if data changes between the final two checks. 2023-04-05 13:13:42 +02:00
darken d3257d6c5e Improve troubleshooting texts 2023-04-05 13:13:42 +02:00
darken afe1399f72 Release: 2.9.0-rc0 2023-04-02 22:25:17 +02:00
Matthias Urhahn 20ce2d3e22 A troubleshooter that automatically goes through different compatibility settings. (#125) 2023-04-02 22:22:15 +02:00
Matthias Urhahn cea8f2290d More IAP improvements (#121)
* More IAP improvements

* More refactoring

* More tolerance towards GPlay API issues.

* Bump billing dependency

* Migrate away from deprecated methods.
2023-03-20 08:21:54 +01:00
darken 1b5b59c41b Release: 2.8.4-rc0 2023-03-13 17:28:28 +01:00
Matthias Urhahn 8df7c19e84 Fix billing client disconnection handling (#117) 2023-03-13 17:28:01 +01:00
darken 88b0069b94 Release: 2.8.3-rc0 2023-03-13 13:48:11 +01:00
Matthias Urhahn 3b8ddc9f06 Fix reversed AirPods Max wear-detection (#116)
* Fix reversed AirPods Max wear-detection
Fixes #115

* Remove unnecessary test
2023-03-13 13:47:32 +01:00
darken 708c7f38ec Merge remote-tracking branch 'origin/main' into main 2023-03-12 14:05:25 +01:00
darken 139daa1cc2 Update full gplay text 2023-03-12 14:05:20 +01:00
Matthias Urhahn b85ac778c0 Address that gplay api can emit multiple times (#114) 2023-03-12 02:58:37 +01:00
Matthias Urhahn a3e2c9e61d Bump dependencies and gradle (#113) 2023-03-11 18:16:24 +01:00
Matthias Urhahn ad78a0570c For some reason we can finish the widget update broadcast twice. (#112)
I can't yet determine why this race-condition occurs, due to the forced timeout, it shouldn't be due to exceeding the receiver ANR window...
2023-03-11 13:01:15 +01:00
Matthias Urhahn 7e6ee274d8 Quick data updates can cause concurrent access and modifying of the notification builder. (#111)
Internally building the notification can then throw a ConcurrentModificationException.
2023-03-11 13:01:05 +01:00
Matthias Urhahn daf7411c36 Show worker foreground notification early. (#110) 2023-03-11 13:00:55 +01:00
darken d3dedbe6cd Release: 2.8.2-rc0 2023-03-10 12:13:30 +01:00
Matthias Urhahn d5411586d5 Update translations (#109)
* Update translations, add Portuguese

* Update translations
2023-03-10 12:12:31 +01:00
Matthias Urhahn 94e2d40554 Fix AirPods Max wear detection. (#108)
Closes #106
2023-03-10 11:30:40 +01:00
Matthias Urhahn db80fa1b46 Wear: Remove unused code (#104)
* Wear: Remove unused code

* Remove more code
2023-03-02 16:39:11 +01:00
Matthias Urhahn 9b569b8d07 Update Turkish translation (#100) 2023-02-20 09:39:27 +01:00
darken 90288880fb Add f-droid as source 2023-02-20 09:14:20 +01:00
darken 870e518ff6 Release: 2.8.1-rc0 2023-02-09 22:01:02 +01:00
darken 6d89169bae Update release.sh script to create default changelogs for f-droid 2023-02-09 21:59:44 +01:00
darken ac77ee2703 Update default changelog for GPlay 2023-02-09 21:53:33 +01:00
181 changed files with 2267 additions and 730 deletions
+18 -17
View File
@@ -7,46 +7,47 @@ on:
branches: [ main ]
jobs:
build-all:
name: Assemble all variants
build-modules:
name: Build apps
strategy:
fail-fast: false
matrix:
flavor: [ Foss,Gplay ]
variant: [ Debug ]
module: [ app,app-wear ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
run: ./gradlew assemble${{ matrix.flavor }}${{ matrix.variant }}
- name: Build module
run: ./gradlew ${{ matrix.module }}:assemble${{ matrix.flavor }}${{ matrix.variant }}
test-all:
name: Run all tests
test-modules:
name: Unit tests
strategy:
fail-fast: false
matrix:
flavor: [ Foss,Gplay ]
variant: [ Release ]
flavor: [ testFoss,testGplay ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run tests (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
run: ./gradlew test${{ matrix.flavor }}${{ matrix.variant }}
- name: Test modules
run: ./gradlew ${{ matrix.flavor }}${{ matrix.variant }}UnitTest
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 11
java-version: 17
- name: Assemble beta APK
if: contains(steps.tagger.outputs.tag, '-beta')
@@ -130,7 +130,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 11
java-version: 17
- name: Set up ruby env
uses: ruby/setup-ruby@v1
+1
View File
@@ -12,3 +12,4 @@
*.jks
.local/*
/fastlane/report.xml
/fastlane/Appfile
+6 -5
View File
@@ -43,12 +43,13 @@ Currently supported models:
## Download
| Source | Status |
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Source | Status |
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [![](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24version)](https://play.google.com/store/apps/details?id=eu.darken.capod) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | |
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | |
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [![](https://img.shields.io/f-droid/v/eu.darken.capod)](https://f-droid.org/en/packages/eu.darken.capod/) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
## Support the project
+1 -1
View File
@@ -1 +1 @@
2.8.0-rc0 20800000
2.9.3-rc0 20903000
+4 -3
View File
@@ -8,6 +8,7 @@ apply(plugin = "dagger.hilt.android.plugin")
android {
compileSdk = ProjectConfig.compileSdk
namespace = "${ProjectConfig.packageName}.common"
defaultConfig {
minSdk = ProjectConfig.minSdk
@@ -33,12 +34,12 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
@@ -0,0 +1,10 @@
import android.content.BroadcastReceiver
import eu.darken.capod.common.debug.logging.log
fun BroadcastReceiver.PendingResult.finish2(): Boolean = try {
finish()
true
} catch (e: IllegalStateException) {
log { "BroadcastReceiver.PendingResult.finish() failed: $e" }
false
}
@@ -32,9 +32,9 @@ class BleScanner @Inject constructor(
@SuppressLint("MissingPermission") fun scan(
filters: Set<ScanFilter>,
scannerMode: ScannerMode = ScannerMode.BALANCED,
disableOffloadFiltering: Boolean = true,
disableOffloadBatching: Boolean = true,
disableDirectScanCallback: Boolean = true,
disableOffloadFiltering: Boolean = false,
disableOffloadBatching: Boolean = false,
disableDirectScanCallback: Boolean = false,
): Flow<Collection<BleScanResult>> = callbackFlow {
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
@@ -1,18 +0,0 @@
package eu.darken.capod.monitor.core
import dagger.BindsInstance
import dagger.hilt.DefineComponent
import dagger.hilt.components.SingletonComponent
@MonitorScope
@DefineComponent(parent = SingletonComponent::class)
interface MonitorComponent {
@DefineComponent.Builder
interface Builder {
fun coroutineScope(@BindsInstance coroutineScope: MonitorCoroutineScope): Builder
fun build(): MonitorComponent
}
}
@@ -1,16 +0,0 @@
package eu.darken.capod.monitor.core
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import kotlinx.coroutines.CoroutineScope
@InstallIn(MonitorComponent::class)
@Module()
abstract class ProcessorModule {
@Binds
@MonitorScope
abstract fun processorScope(scope: MonitorCoroutineScope): CoroutineScope
}
@@ -1,8 +0,0 @@
package eu.darken.capod.monitor.core
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class MonitorScope
@@ -14,6 +14,7 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.replayingShare
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.pods.core.PodDevice
@@ -113,6 +114,7 @@ class PodMonitor @Inject constructor(
disableDirectCallback = useIndirectScanResultCallback,
)
}
.throttleLatest(1000)
.flatMapLatest { options ->
val filters = when {
options.showUnfiltered -> {
@@ -3,6 +3,7 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.pods.core.HasChargeDetection
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
@@ -22,21 +23,18 @@ data class AirPodsMax(
override val proximityMessage: ProximityPairing.Message,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasEarDetection, HasAppleColor {
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
override val rssi: Int
get() = rssiAverage ?: super<SingleApplePods>.rssi
val isHeadphonesBeingWorn: Boolean
get() = rawStatus.isBitSet(1)
val isHeadsetBeingCharged: Boolean
override val isHeadsetBeingCharged: Boolean
get() = rawFlags.isBitSet(0)
override val isBeingWorn: Boolean
get() = isHeadphonesBeingWorn
get() = rawStatus.isBitSet(5)
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Невядомы стан</string>
<string name="last_seen_x">Апошні раз быў: %s</string>
<string name="first_seen_x">Упершыню быў: %s</string>
<string name="permission_post_notifications_label">Адлюстраванне апавяшчэнняў</string>
<string name="permission_post_notifications_description">"Дазволіць CAPod адлюстроўваць вам апавяшчэнні аб вашых AirPods: напрыклад, іх бягучы стан пры падключэнні."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Estat desconegut</string>
<string name="last_seen_x">Últim cop vist: %s</string>
<string name="first_seen_x">Primera vegada vist: %s</string>
<string name="permission_post_notifications_label">Publica notificacions</string>
<string name="permission_post_notifications_description">"Permet que el CAPod us mostri notificacions sobre els vostres AirPods, p. ex. el seu estat actual mentre estan connectats."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Neznámý stav</string>
<string name="last_seen_x">Naposledy spatřeno: %s</string>
<string name="first_seen_x">Poprvé spatřeno: %s</string>
<string name="permission_post_notifications_label">Posílat zprávy</string>
<string name="permission_post_notifications_description">"Povolte aplikaci CAPod zobrazovat oznámení o sluchátkách AirPods, např. o jejich aktuálním stavu během připojení."</string>
</resources>
@@ -7,7 +7,7 @@
<string name="general_error_label">Fehler</string>
<string name="general_grant_permission_action">Berechtigung erteilen</string>
<string name="overview_nomaindevice_label">Kein primäres Gerät</string>
<string name="overview_nomaindevice_description">Alle gefundenen Geräte sind unwahrscheinlich ihnen zu gehören. Verbinden sie ihr Gerät oder passen sie die Einstellungen an.</string>
<string name="overview_nomaindevice_description">Alle gefundenen Geräte gehören wahrscheinlich nicht dir. Schalte deine Kopfhörer ein oder passe die Einstellungen von CAPod an.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth ist deaktiviert</string>
<string name="overview_bluetooth_disabled_description">Bluetooth ist deaktiviert, aktiviere es ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth verbinden</string>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Unbekannter Zustand</string>
<string name="last_seen_x">Zuletzt gesehen: %s</string>
<string name="first_seen_x">Zum ersten Mal gesehen: %s</string>
<string name="permission_post_notifications_label">Benachrichtigungen posten</string>
<string name="permission_post_notifications_description">"Erlauben Sie CAPod, Ihnen Benachrichtigungen über Ihre AirPods anzuzeigen, z.b. ihren aktuellen Status, während sie verbunden sind."</string>
</resources>
+32 -25
View File
@@ -1,36 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_value_not_available_label">N/D</string>
<string name="general_grant_permission_action">Conceder permiso</string>
<string name="overview_nomaindevice_label">Ningún dispositivo principal</string>
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean suyos. Encienda y conecte su dispositivo o ajuste la configuración.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth deshabilitado</string>
<string name="overview_bluetooth_disabled_description">El Bluetooth está deshabilitado, habilítelo ;)</string>
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicación requiere el permiso \"conexión Bluetooth\" para interactuar con los dispositivos pareados, e iniciar las conexiones.</string>
<string name="permission_bluetooth_scan_label">Buscando Bluetooth</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos, como sus AirPods.</string>
<string name="permission_bluetooth_description">Esta aplicacion requiere \"Bluetooth\" para conectarse a los dispositivos pareados.</string>
<string name="permission_access_fine_location_label">Acceso a la ubicación precisa</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso de \"ubicación precisa\" para recibir datos de Bluetooth Low Energy. Sus auriculares utilizan la tecnología Bluetooth Low Energy para transmitir su estado. Esta aplicación NO utilizará datos de Bluetooth para determinar su ubicación.</string>
<string name="permission_background_location_label">Acceso a la ubicación en segundo plano</string>
<string name="permission_background_location_description">CAPod usa el \"Acceso a la ubicación en segundo plano\" para habilitar funciones como \"Mostrar ventana emergente\" y \"Conexión automática\" mientras la aplicación está cerrada. El acceso a la ubicación en segundo plano permite que la aplicación reciba datos de Bluetooth Low Energy mientras está en segundo plano. Esta aplicación NO utilizará datos de Bluetooth para determinar su ubicación.</string>
<string name="permission_ignore_battery_optimizations_label">Desactivar Optimización de Batería</string>
<string name="permission_ignore_battery_optimizations_description">La Optimización de Batería evitan que esta aplicación reciba datos de Bluetooth de manera confiable mientras la aplicación está en segundo plano.</string>
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">No disponible</string>
<string name="general_error_label">Fallo</string>
<string name="general_grant_permission_action">Conceder el permiso</string>
<string name="overview_nomaindevice_label">Sin dispositivo principal</string>
<string name="overview_nomaindevice_description">Es poco probable que todos los dispositivos detectados sean tuyos. Enciende y conecta tu dispositivo o ajusta la configuración.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth desactivado</string>
<string name="overview_bluetooth_disabled_description">El Bluetooth está desactivado, actívalo ;)</string>
<string name="permission_bluetooth_connect_label">Conexión por Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicación requiere el permiso \"Conexión por Bluetooth\" para interactuar con los dispositivos emparejados e iniciar las conexiones.</string>
<string name="permission_bluetooth_scan_label">El Bluetooth está escaneando</string>
<string name="permission_bluetooth_scan_description">El permiso \"Escaneo del Bluetooth\" permite que esta aplicación descubra y reciba datos de Bluetooth de dispositivos cercanos, como tus AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Esta aplicación requiere el permiso \"Bluetooth\" para conectarse a dispositivos bluetooth emparejados.</string>
<string name="permission_access_fine_location_label">Acceso preciso a tu ubicación</string>
<string name="permission_access_fine_location_description">CAPod utiliza el permiso \"localización precisa\" para recibir datos Bluetooth Low Energy. Tus auriculares utilizan la tecnología Bluetooth Low Energy para transmitir tu estado. Esta aplicación NO utilizará los datos Bluetooth para determinar tu ubicación.</string>
<string name="permission_background_location_label">Ubicación en segundo plano</string>
<string name="permission_background_location_description">CAPod utiliza el \"la ubicación en segundo plano\" para activar funciones como \"Mostrar ventana emergente\" y \"Conexión automática\" mientras la aplicación está cerrada. El acceso a la localización en segundo plano permite a la aplicación recibir datos de Bluetooth Low Energy mientras está en segundo plano. Esta aplicación NO utilizará los datos Bluetooth para determinar tu ubicación.</string>
<string name="permission_ignore_battery_optimizations_label">Desactivar las optimizaciones de la batería</string>
<string name="permission_ignore_battery_optimizations_description">Las optimizaciones de la batería impiden que esta aplicación reciba datos Bluetooth de forma fiable mientras está en segundo plano.</string>
<string name="permission_required_title">Se requiere el siguiente permiso:</string>
<string name="permission_system_alert_window_label">Ventana de alerta del sistema</string>
<string name="permission_system_alert_window_description">Permita que CAPod dibuje sobre otras aplicaciones para que la función \"Mostrar ventana emergente\" sea posible.</string>
<string name="permission_system_alert_window_description">Permite a CAPod dibujar sobre otras aplicaciones para hacer posible la función \"Mostrar una ventana emergente\".</string>
<string name="settings_scanner_mode_lowpower_label">Bajo consumo</string>
<string name="settings_scanner_mode_balanced_label">Balanceado</string>
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
<string name="settings_scanner_mode_lowlatency_label">Baja latencia</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 se ve</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>
<string name="pods_dual_left_label">Audífono izquierdo</string>
<string name="pods_dual_right_label">Audífono derecho</string>
<string name="pods_dual_left_label">Auricular izquierdo</string>
<string name="pods_dual_right_label">Auricular derecho</string>
<string name="pods_case_label">Estuche</string>
<string name="pods_case_status_open_label">Abierto</string>
<string name="pods_case_status_closed_label">Cerrado</string>
@@ -40,8 +45,8 @@
<string name="pods_connection_state_call_label">En modo llamada</string>
<string name="pods_connection_state_ringing_label">Sonando</string>
<string name="pods_connection_state_hanging_up_label">Colgar</string>
<string name="pods_connection_state_unknown_label">Estado de la conexion desconocido</string>
<string name="pods_unknown_raw_data_label">Datos sin procesar</string>
<string name="pods_connection_state_unknown_label">Estado de la conexión desconocido</string>
<string name="pods_unknown_raw_data_label">Datos no procesados</string>
<string name="pods_unknown_label">Dispositivo desconocido</string>
<string name="pods_unknown_contact_dev">Este es un dispositivo desconocido, pero está usando un formato de mensaje similar. Agreguemos soporte para ello, contácteme :)</string>
<string name="pods_none_label_short">Sin dispositivo</string>
@@ -49,8 +54,10 @@
<string name="pods_inear_label">En el oído</string>
<string name="pods_microphone_label">Micrófono</string>
<string name="pods_yours">Tuyos</string>
<string name="headset_being_worn_label">En uso</string>
<string name="headset_being_worn_label">Utilizándose</string>
<string name="pods_case_unknown_state">Estado desconocido</string>
<string name="last_seen_x">Visto por última vez: %s</string>
<string name="first_seen_x">Visto por primera vez: %s</string>
<string name="permission_post_notifications_label">Publicar las notificaciones</string>
<string name="permission_post_notifications_description">"Permite que CAPod te muestre notificaciones sobre tus AirPods, por ejemplo, su estado actual mientras están conectados."</string>
</resources>
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">Tasuline CAPod</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Pole saadaval</string>
<string name="pods_dual_left_label">Vasak klapp</string>
<string name="pods_dual_right_label">Parem klapp</string>
<string name="permission_post_notifications_description">"Luba CAPodil näidata AirPodsi teateid, nt praegust olekut ühendatud olles."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">État inconnu</string>
<string name="last_seen_x">Vu pour la dernière fois : %s</string>
<string name="first_seen_x">Vu pour la première fois : %s</string>
<string name="permission_post_notifications_label">Afficher des notifications</string>
<string name="permission_post_notifications_description">"Permettre à CAPod dafficher des notifications au sujet des AirPods, p. ex. leur état actuel alors quils sont connectés."</string>
</resources>
@@ -2,8 +2,8 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">कैपोड</string>
<string name="app_name_pro">कैपोड प्रो</string>
<string name="app_name_foss">कैपोड फोस</string>
<string name="general_value_not_available_label">एन/ए</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N/A</string>
<string name="general_error_label">त्रुटि</string>
<string name="general_grant_permission_action">अनुदान अनुमति</string>
<string name="overview_nomaindevice_label">कोई प्राथमिक उपकरण नहीं</string>
@@ -51,4 +51,13 @@
<string name="pods_unknown_contact_dev">यह एक अज्ञात डिवाइस है, लेकिन यह समान संदेश प्रारूप का उपयोग कर रहा है। आइए इसके लिए समर्थन जोड़ें, मुझसे संपर्क करें :)</string>
<string name="pods_none_label_short">उपकरण नहीं</string>
<string name="pods_charging_label">चार्ज</string>
<string name="pods_inear_label">कान में</string>
<string name="pods_microphone_label">माइक्रोफ़ोन</string>
<string name="pods_yours">आपका</string>
<string name="headset_being_worn_label">पहना जा रहा है</string>
<string name="pods_case_unknown_state">अज्ञात अवस्था</string>
<string name="last_seen_x">अंतिम बार देखे गए: %s</string>
<string name="first_seen_x">पहली बार देखा गया: %s</string>
<string name="permission_post_notifications_label">सूचनाएं जारी करें</string>
<string name="permission_post_notifications_description">"CAPod को अपने AirPods के बारे में सूचनाएं दिखाने की अनुमति दें, जैसे कि जुड़े रहने के दौरान उनकी वर्तमान स्थिति।"</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Status tidak diketahui</string>
<string name="last_seen_x">Terakhir terlihat: %s</string>
<string name="first_seen_x">Pertama kali terlihat: %s</string>
<string name="permission_post_notifications_label">Posting pemberitahuan</string>
<string name="permission_post_notifications_description">"Izinkan CAPod menampilkan pemberitahuan tentang AirPods Anda, mis. status saat terhubung."</string>
</resources>
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod פרו</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">לא זמין</string>
<string name="general_error_label">שגיאה</string>
<string name="general_grant_permission_action">מתן רשות</string>
@@ -55,4 +58,6 @@
<string name="pods_case_unknown_state">מצב לא ידוע</string>
<string name="last_seen_x">נראה לאחרונה: %s</string>
<string name="first_seen_x">נראה לראשונה: %s</string>
<string name="permission_post_notifications_label">הודעות פרסום</string>
<string name="permission_post_notifications_description">"לאשר ל-CAPod להראות לך התראות על ה-AirPods שלך, למשל. המצב הנוכחי שלהם בזמן שהם מחוברים."</string>
</resources>
@@ -55,4 +55,6 @@
<string name="pods_case_unknown_state">Keadaan tidak diketahui</string>
<string name="last_seen_x">Terakhir dilihat: %s</string>
<string name="first_seen_x">Pertama dilihat: %s</string>
<string name="permission_post_notifications_label">Siarkan pemberitahuan</string>
<string name="permission_post_notifications_description">"Benarkan CAPod menunjukkan pemberitahuan kepada anda tentang AirPods anda, mis. status semasa mereka semasa disambungkan."</string>
</resources>
@@ -32,7 +32,7 @@
<string name="settings_monitor_mode_automatic_label">Wanneer het apparaat is aangesloten</string>
<string name="settings_monitor_mode_always_label">Altijd</string>
<string name="settings_reaction_autoconnect_whenseen_label">Wanneer gezien</string>
<string name="settings_reaction_autoconnect_caseopen_label">Case is geopend</string>
<string name="settings_reaction_autoconnect_caseopen_label">Zaak is geopend</string>
<string name="settings_reaction_autoconnect_inear_label">In oor</string>
<string name="pods_dual_left_label">Linker pod</string>
<string name="pods_dual_right_label">Rechter pod</string>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Onbekende staat</string>
<string name="last_seen_x">Laatst gezien: %s</string>
<string name="first_seen_x">Eerst gezien: %s</string>
<string name="permission_post_notifications_label">Post notificaties</string>
<string name="permission_post_notifications_description">"Sta CAPod toe notificaties over je AirPods te tonen, b.v. de huidige verbindingsstatus."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Ukjent status</string>
<string name="last_seen_x">Sist sett: %s</string>
<string name="first_seen_x">Først sett: %s</string>
<string name="permission_post_notifications_label">Vis varsler</string>
<string name="permission_post_notifications_description">"La CAPod vise varsler om dine AirPods, f.eks. nåværende status under tilkobling."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Nieznany stan</string>
<string name="last_seen_x">Ostatnio widziane: %s</string>
<string name="first_seen_x">Widziane pierwszy raz: %s</string>
<string name="permission_post_notifications_label">Wysyłanie powiadomień</string>
<string name="permission_post_notifications_description">"Zezwól CAPod na wyświetlanie powiadomień o słuchawkach AirPods, np. ich aktualnym statusie podczas połączenia."</string>
</resources>
@@ -2,6 +2,8 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">Fóssil CAPod</string>
<string name="general_value_not_available_label">N/A</string>
<string name="general_error_label">Erro</string>
<string name="general_grant_permission_action">Conceder permissão</string>
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
@@ -56,4 +58,6 @@
<string name="pods_case_unknown_state">Estado desconhecido</string>
<string name="last_seen_x">Visto pela última vez: %s</string>
<string name="first_seen_x">Visto pela primeira vez: %s</string>
<string name="permission_post_notifications_label">Post de notificações</string>
<string name="permission_post_notifications_description">"Permita que o CAPod mostre notificações sobre seus AirPods, por exemplo. seu status atual enquanto conectado."</string>
</resources>
+62 -1
View File
@@ -1,2 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">N/A</string>
<string name="general_error_label">Erro</string>
<string name="general_grant_permission_action">Conceder permissão</string>
<string name="overview_nomaindevice_label">Sem dispositivo primário</string>
<string name="overview_nomaindevice_description">É improvável que os dispositivos detetados sejam seus. Ligue e conecte o seu dispositivo ou ajuste as definições.</string>
<string name="overview_bluetooth_disabled_label">O Bluetooth está desligado</string>
<string name="overview_bluetooth_disabled_description">O Bluetooth está desligado, ativa-o ;)</string>
<string name="permission_bluetooth_connect_label">Conexão ao Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicação requer permissão para conectar com o Bluetooth para interagir com dispositivos emparelhados e iniciar conexões.</string>
<string name="permission_bluetooth_scan_label">Explorar Bluetooth</string>
<string name="permission_bluetooth_scan_description">A permissão para explorar o Bluetooth autoriza esta aplicação para descobrir e detetar dados de Bluetooth de dispositivos por perto como os seus AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Esta aplicação requer a permissão do Bluetooth para conectar e emparelhar a dispositivos bluetooth.</string>
<string name="permission_access_fine_location_label">Aceder a localização exata</string>
<string name="permission_access_fine_location_description">CAPod utiliza a localização exata para receber dados de \'Bluetooth Low Energy\'. Os seus headphones utilizam tecnologia \'Bluetooth Low Energy\' para transmitir o seu estado. Esta aplicação NÃO irá utilizar dados de Bluetooth para determinar a sua localização.</string>
<string name="permission_background_location_label">Acesso à localização de fundo</string>
<string name="permission_background_location_description">CAPod utiliza o acesso à localização de fundo para habilitar funcionalidades como \"Mostrar pop-up\" e \"AutoConectar\" enquanto a aplicação está fechada. Acesso à localização de fundo autoriza à aplicação receber dados de \'Bluetooth Low Energy\' enquanto está de fundo. Esta aplicação NÃO irá utilizar dados de Bluetooth para determinar a sua localização.</string>
<string name="permission_ignore_battery_optimizations_label">Desativar otimizações de bateria</string>
<string name="permission_ignore_battery_optimizations_description">Otimizações de bateria impedem que esta aplicação receba dados de Bluetooth fidedignos enquanto a aplicação está de fundo.</string>
<string name="permission_required_title">A seguinte permissão é necessária:</string>
<string name="permission_system_alert_window_label">Janela de Alerta do Sistema</string>
<string name="permission_system_alert_window_description">Autorizar CAPod para aparecer por cima de outras aplicações para tornar possível a funcionalidade \"Mostrar pop-up\".</string>
<string name="settings_scanner_mode_lowpower_label">Poupar bateria</string>
<string name="settings_scanner_mode_balanced_label">Equilibrado</string>
<string name="settings_scanner_mode_lowlatency_label">Baixa latência</string>
<string name="settings_monitor_mode_manual_label">Quando a aplicação está aberta</string>
<string name="settings_monitor_mode_automatic_label">Quando o dispositivo é conectado</string>
<string name="settings_monitor_mode_always_label">Sempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Quando visto</string>
<string name="settings_reaction_autoconnect_caseopen_label">Caixa está aberta</string>
<string name="settings_reaction_autoconnect_inear_label">No ouvido</string>
<string name="pods_dual_left_label">Pod esquerdo</string>
<string name="pods_dual_right_label">Pod direito</string>
<string name="pods_case_label">Caixa</string>
<string name="pods_case_status_open_label">Aberta</string>
<string name="pods_case_status_closed_label">Fechada</string>
<string name="pods_connection_state_disconnected_label">Não está conectado a um dispositivo</string>
<string name="pods_connection_state_idle_label">Conectado a um dispositivo, mas inativo</string>
<string name="pods_connection_state_music_label">Em modo de música</string>
<string name="pods_connection_state_call_label">Em modo de chamada</string>
<string name="pods_connection_state_ringing_label">A tocar</string>
<string name="pods_connection_state_hanging_up_label">A terminar a chamada</string>
<string name="pods_connection_state_unknown_label">Estado de conexão desconhecido</string>
<string name="pods_unknown_raw_data_label">Dados não tratados</string>
<string name="pods_unknown_label">Dispositivo desconhecido</string>
<string name="pods_unknown_contact_dev">Este é um dispositivo desconhecido, mas usa um formato de mensagem semelhante. Vamos adicionar suporte para isso, entre em contacto comigo :)</string>
<string name="pods_none_label_short">Sem dispositivo</string>
<string name="pods_charging_label">A carregar</string>
<string name="pods_inear_label">No ouvido</string>
<string name="pods_microphone_label">Microfone</string>
<string name="pods_yours">Seu</string>
<string name="headset_being_worn_label">A ser usado</string>
<string name="pods_case_unknown_state">Estado desconhecido</string>
<string name="last_seen_x">Última vez visto: %s</string>
<string name="first_seen_x">Primeira vez visto: %s</string>
<string name="permission_post_notifications_label">Enviar notificações</string>
<string name="permission_post_notifications_description">"Permitir que o CAPod envie notificações sobre os seus AirPods, por exemplo, o seu estado atual enquanto conectado."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Неизвестное состояние</string>
<string name="last_seen_x">Последние: %s</string>
<string name="first_seen_x">Первые: %s</string>
<string name="permission_post_notifications_label">Уведомления о публикации</string>
<string name="permission_post_notifications_description">"Разрешить CAPod показывать уведомления о ваших AirPods, например текущий статус при подключении."</string>
</resources>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">Neznámy stav</string>
<string name="last_seen_x">Naposledy videné: %s</string>
<string name="first_seen_x">Prvýkrát videné: %s</string>
<string name="permission_post_notifications_label">Zobraziť oznámenia</string>
<string name="permission_post_notifications_description">"Povoľte CAPod, aby vám zobrazoval upozornenia týkajúce sa vašich slúchadiel AirPods, napr. ich aktuálny stav počas pripojenia."</string>
</resources>
+31 -25
View File
@@ -1,26 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_value_not_available_label">Yok</string>
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Kapalı</string>
<string name="general_error_label">Hata</string>
<string name="general_grant_permission_action">İzin ver</string>
<string name="overview_nomaindevice_label">Birincil aygıt yok</string>
<string name="overview_nomaindevice_description">Algılanan tüm cihazların sizin olması olası değildir. Cihazınızı açın ve bağlayın veya ayarları yapın.</string>
<string name="overview_nomaindevice_description">Tespit edilen tüm aygıtların sizin olması muhtemel değildir. Aygıtınızı açın ve bağlayın veya ayarları yapın.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth devre dışı</string>
<string name="overview_bluetooth_disabled_description">Bluetooth devre dışı, etkinleştirin ;)</string>
<string name="overview_bluetooth_disabled_description">Bluetooth devre dışı, lütfen etkinleştirin ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth bağlantısı</string>
<string name="permission_bluetooth_connect_description">Bu uygulama, eşleştirilmiş cihazlarla etkileşim kurmak ve bağlantıları başlatmak için \"Bluetooth bağlantısı\" izni gerektirir.</string>
<string name="permission_bluetooth_connect_description">Bu uygulama, eşleştirilmiş aygıtlarla etkileşime geçmek ve bağlantıları başlatmak için \"Bluetooth bağlantısı\" izni gerektirir.</string>
<string name="permission_bluetooth_scan_label">Bluetooth taraması</string>
<string name="permission_bluetooth_scan_description">\"Bluetooth tarama\" izni, bu uygulamanın AirPods\'unuz gibi yakındaki cihazlardan Bluetooth verilerini keşfetmesine ve almasına izin verir.</string>
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth cihazlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
<string name="permission_access_fine_location_label">Hassas konuma erişin</string>
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"iyi konum\" iznini kullanır. Kulaklıklarınız durumlarını yayınlamak için Bluetooth Düşük Enerji teknolojisini kullanır. Bu uygulama konumunuzu belirlemek için Bluetooth verilerini KULLANMAYACAKTIR.</string>
<string name="permission_bluetooth_scan_description">\"Bluetooth tarama\" izni, bu uygulamanın AirPod\'larınız gibi yakındaki aygıtlardan Bluetooth verilerini keşfetmesine ve almasına olanak tanır.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Bu uygulama, eşleştirilmiş bluetooth aygıtlarına bağlanmak için \"Bluetooth\" izni gerektirir.</string>
<string name="permission_access_fine_location_label">Tam konuma erişim</string>
<string name="permission_access_fine_location_description">CAPod, Bluetooth Düşük Enerji verilerini almak için \"tam konum\" iznini kullanır. Kulaklığınız, durumlarını yayınlamak için Bluetooth Düşük Enerji teknolojisini kullanır. Bu uygulama, konumunuzu belirlemek için Bluetooth verilerini KULLANMAZ.</string>
<string name="permission_background_location_label">Arka planda konum erişimi</string>
<string name="permission_background_location_description">CAPod, uygulama kapalıyken \"Açılır pencereyi göster\" ve \"Otomatik Bağlantı\" gibi özellikleri etkinleştirmek için \"arka plan konumu erişimi\" kullanır. Arka plan konumu erişimi, uygulamanın arka plandayken Bluetooth Düşük Enerji verilerini almasını sağlar. Bu uygulama konumunuzu belirlemek için Bluetooth verilerini KULLANMAZ.</string>
<string name="permission_background_location_description">CAPod, uygulama kapalıyken \"Açılır pencereyi göster\" ve \"Otomatik Bağlantı\" gibi özellikleri etkinleştirmek için \"arka planda konum erişimi\"ni kullanır. Arka planda konum erişimi, uygulamanın arka plandayken Bluetooth Düşük Enerji verilerini almasına olanak tanır. Bu uygulama, konumunuzu belirlemek için Bluetooth verilerini KULLANMAZ.</string>
<string name="permission_ignore_battery_optimizations_label">Pil optimizasyonlarını devre dışı bırak</string>
<string name="permission_ignore_battery_optimizations_description">Pil iyileştirmeleri, uygulama arka plandayken bu uygulamanın Bluetooth verilerini güvenilir bir şekilde almasını engeller.</string>
<string name="permission_ignore_battery_optimizations_description">Pil optimizasyonları, uygulamanın arka planda çalışırken güvenilir bir şekilde Bluetooth verilerini almasını engeller.</string>
<string name="permission_required_title">Aşağıdaki izin gereklidir:</string>
<string name="permission_system_alert_window_label">Sistem Uyarı Penceresi</string>
<string name="permission_system_alert_window_description">CAPod\'un \"PopUp\'ı Göster\" özelliğini mümkün kılmak için diğer uygulamaların üzerinde çizmesine izin verin.</string>
<string name="permission_system_alert_window_description">\"Açılır Pencere Göster\" özelliğine izin vermek için CAPod\'un diğer uygulamaların üzerinde gösterilmesine izin vermelisiniz.</string>
<string name="settings_scanner_mode_lowpower_label">Düşük güç</string>
<string name="settings_scanner_mode_balanced_label">Dengeli</string>
<string name="settings_scanner_mode_lowlatency_label">Düşük gecikme süresi</string>
@@ -28,30 +32,32 @@
<string name="settings_monitor_mode_automatic_label">Aygıt bağlandığında</string>
<string name="settings_monitor_mode_always_label">Her zaman</string>
<string name="settings_reaction_autoconnect_whenseen_label">Görüldüğünde</string>
<string name="settings_reaction_autoconnect_caseopen_label">Vaka açık</string>
<string name="settings_reaction_autoconnect_caseopen_label">Şarj kutusu açık</string>
<string name="settings_reaction_autoconnect_inear_label">Kulakta</string>
<string name="pods_dual_left_label">Sol bölme</string>
<string name="pods_dual_right_label">Sağ bölme</string>
<string name="pods_case_label">Sorun</string>
<string name="pods_dual_left_label">Sol kulaklık</string>
<string name="pods_dual_right_label">Sağ kulaklık</string>
<string name="pods_case_label">Şarj kutusu</string>
<string name="pods_case_status_open_label">Açık</string>
<string name="pods_case_status_closed_label">Kapalı</string>
<string name="pods_connection_state_disconnected_label">Bir cihaza bağlı değil</string>
<string name="pods_connection_state_idle_label">Bir cihaza bağlı, ancak boşta</string>
<string name="pods_connection_state_disconnected_label">Bir aygıta bağlı değil</string>
<string name="pods_connection_state_idle_label">Bir aygıta bağlı, ancak boşta</string>
<string name="pods_connection_state_music_label">Müzik modunda</string>
<string name="pods_connection_state_call_label">Arama modunda</string>
<string name="pods_connection_state_ringing_label">Çalıyor</string>
<string name="pods_connection_state_hanging_up_label">Kapatıyorum</string>
<string name="pods_connection_state_hanging_up_label">Kapatılıyor</string>
<string name="pods_connection_state_unknown_label">Bilinmeyen bağlantı durumu</string>
<string name="pods_unknown_raw_data_label">İşlenmemiş veri</string>
<string name="pods_unknown_label">Tanımlanmamış cihaz</string>
<string name="pods_unknown_contact_dev">Bu bilinmeyen bir cihazdır, ancak benzer bir mesaj formatı kullanıyor. Bunun için destek ekleyelim, bana ulaşın :)</string>
<string name="pods_none_label_short">Cihaz yok</string>
<string name="pods_charging_label">Doluyor</string>
<string name="pods_unknown_raw_data_label">Ham veri</string>
<string name="pods_unknown_label">Bilinmeyen aygıt</string>
<string name="pods_unknown_contact_dev">Bu bilinmeyen bir aygıt ama benzer mesaj formatı kullanıyor. Hadi buna destek ekleyelim, bana ulaşın :)</string>
<string name="pods_none_label_short">Aygıt yok</string>
<string name="pods_charging_label">Şarj oluyor</string>
<string name="pods_inear_label">Kulakta</string>
<string name="pods_microphone_label">Mikrofon</string>
<string name="pods_yours">Senin</string>
<string name="headset_being_worn_label">Giyilmek</string>
<string name="pods_yours">Sizinki</string>
<string name="headset_being_worn_label">Takılıyken</string>
<string name="pods_case_unknown_state">Bilinmeyen durum</string>
<string name="last_seen_x">Son görülme: %s</string>
<string name="first_seen_x">İlk görülme: %s</string>
<string name="permission_post_notifications_label">Gönderi bildirimleri</string>
<string name="permission_post_notifications_description">"CAPod'un size AirPod'larınız hakkında bildirimler göstermesine izin verin, ör. bağlıyken geçerli durumları."</string>
</resources>
+23 -2
View File
@@ -1,23 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="general_value_not_available_label">Н</string>
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">Н</string>
<string name="general_error_label">Помилка</string>
<string name="general_grant_permission_action">Надати дозвіл</string>
<string name="overview_nomaindevice_label">Відсутній первинний пристрій</string>
<string name="overview_nomaindevice_description">Всі знайдені пристрої навряд чи ваші. Увімкніть та підʼєднайте ваш пристрій або налаштуйте параметри.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth вимкнено</string>
<string name="overview_bluetooth_disabled_description">Bluetooth вимкнено, увімкніть його ;)</string>
<string name="permission_bluetooth_connect_label">Bluetooth зʼєднання</string>
<string name="permission_bluetooth_connect_description">Цей додаток потребує дозвіл \"Bluetooth зʼєднання\" щоб взаємодіяти зі сполученими пристроями та ініціювати нові зʼєднання.</string>
<string name="permission_bluetooth_scan_label">Сканування Bluetooth</string>
<string name="permission_bluetooth_scan_description">Дозвіл \"Сканування Bluetooth\" дозволяє цьому додатку виявляти та отримувати Bluetooth дані з пристроїв поблизу, таких як ваші AirPods.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Цей додаток потребує дозвіл \"Bluetooth\" для зʼєднання зі сполученими Bluetooth пристроями.</string>
<string name="permission_access_fine_location_label">Доступ до точного місцезнаходження</string>
<string name="permission_access_fine_location_description">CAPod використовує дозвіл \"Точне місцезнаходження\" щоб отримувати дані Bluetooth Low Energy. Ваші навушники використовують технологію Bluetooth Low Energy technology щоб транслювати свій статус. Цей додаток НЕ використовує Bluetooth дані щоб визначати ваше місцеперебування.</string>
<string name="permission_background_location_label">Доступ до місцезнаходження у тлі</string>
<string name="permission_background_location_description">CAPod використовує фоновий доступ до геоданих для роботи таких функцій як \"Показувати спливаюче вікно\" та \"Автопідʼєднання\" коли додаток закритий. Фоновий доступ до геоданих дозволяє додатку отримувати дані Bluetooth Low Energy коли він працює у фоновому режимі. Цей додаток НЕ використовує Bluetooth дані щоб визначати ваше місцеперебування.</string>
<string name="permission_ignore_battery_optimizations_label">Вимкнути оптимізацію батареї</string>
<string name="permission_ignore_battery_optimizations_description">Оптимізація батареї перешкоджає цьому пристрою надійно отримувати дані Bluetooth під час перебування програми у тлі.</string>
<string name="permission_required_title">Необхідні наступні дозволи:</string>
<string name="permission_system_alert_window_label">Вікно системного повідомлення</string>
<string name="settings_scanner_mode_lowpower_label">Низький заряд</string>
<string name="permission_system_alert_window_description">Надайте дозвіл CAPod на показ поверх інших додатків для роботи функції \"Показувати спливаюче вікно\".</string>
<string name="settings_scanner_mode_lowpower_label">Економія енергії</string>
<string name="settings_scanner_mode_balanced_label">Збалансовано</string>
<string name="settings_scanner_mode_lowlatency_label">Низька затримка</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>
<string name="pods_dual_left_label">Лівий навушник</string>
@@ -26,9 +40,13 @@
<string name="pods_case_status_open_label">Відкрито</string>
<string name="pods_case_status_closed_label">Закрито</string>
<string name="pods_connection_state_disconnected_label">Не під\'єднано до пристрою</string>
<string name="pods_connection_state_idle_label">Підʼєднано до пристрою, в режимі очікування</string>
<string name="pods_connection_state_music_label">У режимі музики</string>
<string name="pods_connection_state_call_label">У режимі виклику</string>
<string name="pods_connection_state_ringing_label">Дзвінок</string>
<string name="pods_connection_state_hanging_up_label">Кінець дзвінка</string>
<string name="pods_connection_state_unknown_label">Невідомий стан з\'єднання</string>
<string name="pods_unknown_raw_data_label">Сирі дані</string>
<string name="pods_unknown_label">Невідомий пристрій</string>
<string name="pods_unknown_contact_dev">Це невідомий пристрій, проте він використовує подібний формат повідомлень. Додаймо його підтримку, надішліть мені лист :)</string>
<string name="pods_none_label_short">Немає пристрою</string>
@@ -36,7 +54,10 @@
<string name="pods_inear_label">У вусі</string>
<string name="pods_microphone_label">Мікрофон</string>
<string name="pods_yours">Ваш</string>
<string name="headset_being_worn_label">Одягнені</string>
<string name="pods_case_unknown_state">Невідомий стан</string>
<string name="last_seen_x">Востаннє був: %s</string>
<string name="first_seen_x">Уперше був: %s</string>
<string name="permission_post_notifications_label">Показ сповіщень</string>
<string name="permission_post_notifications_description">"Дозволити CAPod показувати вам сповіщення щодо ваших AirPods, наприклад, їх поточний стан при підключенні."</string>
</resources>
@@ -1,2 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"></resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">不適用</string>
<string name="general_error_label">錯誤</string>
<string name="general_grant_permission_action">授予權限</string>
<string name="overview_nomaindevice_label">無主要裝置</string>
<string name="overview_nomaindevice_description">所有偵測到的裝置似乎並不是您的。開啟並連線至您的裝置,也可以調整設定。</string>
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
<string name="permission_bluetooth_connect_label">藍牙連線</string>
<string name="permission_bluetooth_connect_description">這個應用程式需要「藍牙連線」權限以與已配對的裝置交互或配對新裝置。</string>
<string name="permission_bluetooth_scan_label">藍牙掃描</string>
<string name="permission_bluetooth_scan_description">「藍牙掃描」權限允許這個應用程式發現附近的裝置並接收藍牙資料,您的 AirPods 需要如此。</string>
<string name="permission_bluetooth_label">藍牙</string>
<string name="permission_bluetooth_description">這個應用程式需要「藍牙」權限與已配對裝置連線。</string>
<string name="permission_access_fine_location_label">存取精確位置</string>
<string name="permission_access_fine_location_description">CAPod 使用「精確位置」權限以接收低功耗藍牙資料。您的耳機使用低功耗藍牙技術以廣播其狀態。這個應用程式不會使用藍牙資料來確定您的位置。</string>
<string name="permission_background_location_label">背景位置存取</string>
<string name="permission_background_location_description">CAPods 在應用程式關閉時使用「背景位置存取」來啟用諸如「顯示彈出式視窗」和「自動連線」等功能。背景位置存取允許這個應用程式在背景接收低功耗藍牙資料。這個應用程式不會使用藍牙資料來確定您的位置。</string>
<string name="permission_ignore_battery_optimizations_label">停用電池效能最佳化</string>
<string name="permission_ignore_battery_optimizations_description">電池效能最佳化使這個應用程式在背景時無法可靠地接收藍牙資料。</string>
<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_scanner_mode_lowpower_label">低電源</string>
<string name="settings_scanner_mode_balanced_label">平衡</string>
<string name="settings_scanner_mode_lowlatency_label">低延遲</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>
<string name="pods_dual_left_label">左耳</string>
<string name="pods_dual_right_label">右耳</string>
<string name="pods_case_label">充電盒</string>
<string name="pods_case_status_open_label">已開啟</string>
<string name="pods_case_status_closed_label">已關閉</string>
<string name="pods_connection_state_disconnected_label">未連線到裝置</string>
<string name="pods_connection_state_idle_label">已連線到裝置,閒置中</string>
<string name="pods_connection_state_music_label">音樂模式</string>
<string name="pods_connection_state_call_label">呼叫模式</string>
<string name="pods_connection_state_ringing_label">振鈴</string>
<string name="pods_connection_state_hanging_up_label">掛機</string>
<string name="pods_connection_state_unknown_label">未知連線狀態</string>
<string name="pods_unknown_raw_data_label">原始資料</string>
<string name="pods_unknown_label">未知裝置</string>
<string name="pods_unknown_contact_dev">這是一個未知裝置,但可以使用相似的訊息格式。聯絡我以為此裝置新增相關支援 :)</string>
<string name="pods_none_label_short">無裝置</string>
<string name="pods_charging_label">充電中</string>
<string name="pods_inear_label">在耳中</string>
<string name="pods_microphone_label">麥克風</string>
<string name="pods_yours">您的</string>
<string name="headset_being_worn_label">正被佩戴</string>
<string name="pods_case_unknown_state">未知狀態</string>
<string name="last_seen_x">最後連線:%s</string>
<string name="first_seen_x">首次連線:%s</string>
<string name="permission_post_notifications_label">張貼通知</string>
<string name="permission_post_notifications_description">"允許 CAPod 為您顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
</resources>
@@ -3,11 +3,11 @@
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label"></string>
<string name="general_value_not_available_label"></string>
<string name="general_error_label">錯誤</string>
<string name="general_grant_permission_action">授予權限</string>
<string name="overview_nomaindevice_label">無主要裝置</string>
<string name="overview_nomaindevice_description">所有偵測到的裝置似乎並不是您的。開啟並連線您的裝置,也可以調整設定。</string>
<string name="overview_nomaindevice_description">所有偵測到的裝置似乎並不是您的。開啟並連線您的裝置,也可以調整設定。</string>
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
<string name="permission_bluetooth_connect_label">藍牙連線</string>
@@ -25,7 +25,7 @@
<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_scanner_mode_lowpower_label">節電</string>
<string name="settings_scanner_mode_lowpower_label">低電源</string>
<string name="settings_scanner_mode_balanced_label">平衡</string>
<string name="settings_scanner_mode_lowlatency_label">低延遲</string>
<string name="settings_monitor_mode_manual_label">應用程式開啟時</string>
@@ -58,4 +58,6 @@
<string name="pods_case_unknown_state">未知狀態</string>
<string name="last_seen_x">最後連線:%s</string>
<string name="first_seen_x">首次連線:%s</string>
<string name="permission_post_notifications_label">張貼通知</string>
<string name="permission_post_notifications_description">"允許 CAPod 為您顯示關於您 AirPods 的通知,例如在連線時顯示它們的目前狀態。"</string>
</resources>
+2 -2
View File
@@ -65,6 +65,6 @@
<string name="last_seen_x">Last seen: %s</string>
<string name="first_seen_x">First seen: %s</string>
<string name="permission_post_notifications_label">Post notifications</string>
<string name="permission_post_notifications_description">"Allow CAPod to show you notifications about your AirPods, e.g. their current status while connected."</string>
<string name="permission_post_notifications_label">Show notifications</string>
<string name="permission_post_notifications_description">"Allow CAPod to show notifications about your AirPods, e.g. their current status while connected."</string>
</resources>
@@ -1,6 +1,5 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@@ -21,15 +20,4 @@ class SingleApplePodsTest : BaseAirPodsTest() {
rawSuffix shouldBe 0x40.toUByte()
}
}
@Test
fun `test values based on AirPodMax`() = runTest {
create<AirPodsMax>("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") {
batteryHeadsetPercent shouldBe 0.5f
isHeadsetBeingCharged shouldBe false
isHeadphonesBeingWorn shouldBe true
}
}
}
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.common.isBitSet
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.HasAppleColor
@@ -27,8 +28,6 @@ class AirPodsMaxTest : BaseAirPodsTest() {
isHeadsetBeingCharged shouldBe false
isHeadphonesBeingWorn shouldBe true
model shouldBe PodDevice.Model.AIRPODS_MAX
}
}
@@ -50,8 +49,6 @@ class AirPodsMaxTest : BaseAirPodsTest() {
batteryHeadsetPercent shouldBe 0.5f
isHeadsetBeingCharged shouldBe false
isHeadphonesBeingWorn shouldBe true
}
}
@@ -72,9 +69,25 @@ class AirPodsMaxTest : BaseAirPodsTest() {
isHeadsetBeingCharged shouldBe false
isHeadphonesBeingWorn shouldBe true
rawStatus.isBitSet(5) shouldBe true
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
}
}
@Test
fun `wear status`() = runTest {
create<AirPodsMax>("07 19 01 0A 20 03 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
rawStatus shouldBe 0x03.toUByte()
rawStatus.isBitSet(5) shouldBe false
isBeingWorn shouldBe false
}
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
rawStatus shouldBe 0x23.toUByte()
rawStatus.isBitSet(5) shouldBe true
isBeingWorn shouldBe true
}
}
}
+6 -3
View File
@@ -9,6 +9,7 @@ apply(plugin = "androidx.navigation.safeargs.kotlin")
android {
compileSdk = ProjectConfig.compileSdk
namespace = "${ProjectConfig.packageName}"
defaultConfig {
applicationId = ProjectConfig.packageName
@@ -102,12 +103,12 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
@@ -132,6 +133,8 @@ dependencies {
addBaseAndroid()
addBaseAndroidUi()
implementation("androidx.core:core-splashscreen:1.0.1")
addNavigation()
addTesting()
@@ -6,8 +6,6 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.hilt.EntryPoints
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
@@ -15,9 +13,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.main.core.GeneralSettings
import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.monitor.core.MonitorComponent
import eu.darken.capod.monitor.core.MonitorCoroutineScope
import eu.darken.capod.monitor.core.PodMonitor
import kotlinx.coroutines.*
@@ -31,22 +27,12 @@ import kotlinx.coroutines.flow.take
class MonitorWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
monitorComponentBuilder: MonitorComponent.Builder,
private val dispatcherProvider: DispatcherProvider,
private val generalSettings: GeneralSettings,
private val permissionTool: PermissionTool,
private val podMonitor: PodMonitor,
private val bluetoothManager: BluetoothManager2,
) : CoroutineWorker(context, params) {
private val workerScope = MonitorCoroutineScope()
private val monitorComponent = monitorComponentBuilder
.coroutineScope(workerScope)
.build()
private val entryPoint by lazy {
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
}
private var finishedWithError = false
@@ -1,9 +0,0 @@
package eu.darken.capod.wear.core
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import eu.darken.capod.monitor.core.MonitorComponent
@InstallIn(MonitorComponent::class)
@EntryPoint
interface MonitorWorkerEntryPoint
@@ -0,0 +1,18 @@
package eu.darken.capod.wear.core
import android.content.Context
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
data class UserTime(
val time: Instant = Instant.now(),
) {
fun toFormatted(context: Context): String = DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
.format(time)
}
@@ -13,14 +13,11 @@ import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.common.livedata.SingleLiveEvent
import eu.darken.capod.common.permissions.Permission
import eu.darken.capod.common.uix.ViewModel3
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.monitor.core.PodDeviceCache
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.PodFactory
import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.wear.core.UserTime
import eu.darken.capod.wear.ui.overview.cards.BluetoothDisabledVH
import eu.darken.capod.wear.ui.overview.cards.MissingMainDeviceVH
import eu.darken.capod.wear.ui.overview.cards.PermissionCardVH
@@ -38,11 +35,8 @@ class OverviewFragmentVM @Inject constructor(
dispatcherProvider: DispatcherProvider,
private val podMonitor: PodMonitor,
private val permissionTool: PermissionTool,
private val generalSettings: GeneralSettings,
debugSettings: DebugSettings,
private val bluetoothManager: BluetoothManager2,
private val podDeviceCache: PodDeviceCache,
private val podFactory: PodFactory
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
private val updateTicker = channelFlow<Unit> {
@@ -54,30 +48,14 @@ class OverviewFragmentVM @Inject constructor(
val requestPermissionEvent = SingleLiveEvent<Permission>()
private val pods: Flow<List<PodDevice>> = permissionTool.missingPermissions
.flatMapLatest { permissions ->
if (permissions.isNotEmpty()) {
return@flatMapLatest flowOf(emptyList())
}
generalSettings.showAll.flow.flatMapLatest { showAll ->
if (showAll) {
podMonitor.devices
} else {
podMonitor.mainDevice.map { mainDevice ->
mainDevice?.let { listOf(it) } ?: emptyList()
}
}
}
}
.catch { errorEvents.postValue(it) }
private val mainDevice = podMonitor.mainDevice.throttleLatest(1000)
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
updateTicker,
permissionTool.missingPermissions,
debugSettings.isDebugModeEnabled.flow,
bluetoothManager.isBluetoothEnabled,
podMonitor.mainDevice.throttleLatest(1000),
mainDevice,
) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
val items = mutableListOf<OverviewAdapter.Item>()
@@ -109,14 +87,20 @@ class OverviewFragmentVM @Inject constructor(
device = podToShow,
showDebug = isDebugMode,
isMainPod = true,
userTime = UserTime(),
)
is SinglePodDevice -> SinglePodsCardVH.Item(
now = now,
device = podToShow,
showDebug = isDebugMode,
isMainPod = true,
userTime = UserTime(),
)
else -> MissingMainDeviceVH.Item(
userTime = UserTime(),
)
else -> MissingMainDeviceVH.Item
}
items.add(pod)
@@ -5,6 +5,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.common.lists.differ.DifferItem
import eu.darken.capod.databinding.OverviewNomaindeviceItemBinding
import eu.darken.capod.wear.core.UserTime
import eu.darken.capod.wear.ui.overview.OverviewAdapter
class MissingMainDeviceVH(parent: ViewGroup) :
@@ -21,11 +22,14 @@ class MissingMainDeviceVH(parent: ViewGroup) :
item: Item,
payloads: List<Any>
) -> Unit = binding(payload = true) { item ->
userTime.text = item.userTime.toFormatted(context)
}
object Item : OverviewAdapter.Item {
override val stableId: Long = Item::class.hashCode().toLong()
data class Item(
val userTime: UserTime,
) : OverviewAdapter.Item {
override val stableId: Long = this.hashCode().toLong()
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
get() = { old, new -> if (new::class.isInstance(old)) new else null }
@@ -9,6 +9,7 @@ import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.wear.core.UserTime
import java.time.Instant
class DualPodsCardVH(parent: ViewGroup) :
@@ -20,7 +21,10 @@ class DualPodsCardVH(parent: ViewGroup) :
override val viewBinding = lazy { OverviewPodsDualItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
userTime.text = item.userTime.toFormatted(context)
val device = item.device
name.apply {
val sb = StringBuilder(device.getLabel(context))
if (device is HasPodStyle && item.showDebug) {
@@ -127,5 +131,6 @@ class DualPodsCardVH(parent: ViewGroup) :
override val device: DualPodDevice,
override val showDebug: Boolean,
override val isMainPod: Boolean,
val userTime: UserTime,
) : PodDeviceVH.Item
}
@@ -3,13 +3,12 @@ package eu.darken.capod.wear.ui.overview.cards.pods
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
import eu.darken.capod.R
import eu.darken.capod.common.lists.BindableVH
import eu.darken.capod.common.lists.differ.DifferItem
import eu.darken.capod.common.lists.modular.ModularAdapter
import eu.darken.capod.wear.ui.overview.OverviewAdapter
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.wear.ui.overview.OverviewAdapter
import java.time.Instant
abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
@@ -19,7 +18,7 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
fun Item.getReceptionText(): String = device.getSignalQuality(context)
.let { if (showDebug) "$it ${device.seenCounter}" else it }
.let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it }
.let { if (isMainPod) "$it\n(${getString(eu.darken.capod.common.R.string.pods_yours)})" else it }
interface Item : OverviewAdapter.Item {
@@ -8,6 +8,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsSingleItemBinding
import eu.darken.capod.pods.core.*
import eu.darken.capod.wear.core.UserTime
import java.time.Instant
class SinglePodsCardVH(parent: ViewGroup) :
@@ -19,6 +20,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
override val viewBinding = lazy { OverviewPodsSingleItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
userTime.text = item.userTime.toFormatted(context)
val device = item.device
name.apply {
@@ -75,5 +78,6 @@ class SinglePodsCardVH(parent: ViewGroup) :
override val device: SinglePodDevice,
override val showDebug: Boolean,
override val isMainPod: Boolean,
val userTime: UserTime,
) : PodDeviceVH.Item
}
@@ -1,44 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M400.08,358.82l-220.33,59a58.52,58.52 0,0 1,-71.68 -41.38l-5,-18.53a171.22,171.22 0,0 1,24.44 -141.76l-23.43,-23.5a25,25 0,0 1,0 -35.35l0.82,-0.82a24.87,24.87 0,0 1,35.24 0l22.27,22.34a170.85,170.85 0,0 1,62 -31.09l0.48,-0.13a172.54,172.54 0,0 1,71.4 -3.89l8.63,-31.85a25.14,25.14 0,0 1,30.7 -17.66l1.13,0.29A24.8,24.8 0,0 1,354.42 125l-9.23,34.07a172.66,172.66 0,0 1,91.7 111l4.58,17.08A58.54,58.54 0,0 1,400.08 358.82Z"
android:fillColor="#41eb8e"
android:fillType="evenOdd" />
<path
android:pathData="M279.56,314.16c-13.7,-8 -31.7,-7.91 -45.66,-0.68L212,351.37c0.68,15.16 9.41,30.14 24.29,38.79a49.92,49.92 0,0 0,14.87 5.76l-20.81,36.44a8.28,8.28 0,0 0,3 11.33l12.8,7.44a8.26,8.26 0,0 0,11.31 -3l41.73,-72.75h0C310.82,354.32 306.77,330 279.56,314.16ZM247.67,347.24a6,6 0,1 1,-2.16 -8.26A6,6 0,0 1,247.67 347.24Z"
android:fillColor="#fff"
android:fillType="evenOdd" />
<path
android:pathData="M300.66,214.18a27.95,27.95 0,1 1,-19.8 34.24A28,28 0,0 1,300.66 214.18Z"
android:fillColor="#26845c"
android:fillType="evenOdd" />
<path
android:pathData="M194.53,242.62a27.95,27.95 0,1 1,-19.8 34.24A28,28 0,0 1,194.53 242.62Z"
android:fillColor="#26845c"
android:fillType="evenOdd" />
<path
android:pathData="M149.79,318.88l-36.3,-21.21a50.22,50.22 0,0 0,10 -12.43c8.56,-14.94 8.61,-32.3 1.59,-45.79l-38,-21.9c-15.72,0.7 -31.35,9.65 -39.22,23.41 -15.64,27.32 -6.94,50.45 13.74,62.85h0l72.64,42.25a8.26,8.26 0,0 0,11.31 -3l7.36,-12.85A8.31,8.31 0,0 0,149.79 318.88ZM100.3,251.53A6.06,6.06 0,1 1,98 243.28,6.07 6.07,0 0,1 100.3,251.53Z"
android:fillColor="#fff"
android:fillType="evenOdd" />
<path
android:pathData="M187.92,204.08a29.52,29.52 0,0 1,-38.6 -15.52A29.15,29.15 0,0 1,165 150.28a30.38,30.38 0,0 1,4.68 -1.52l-16.36,-38.48L148.6,99.07l-4.77,-11.21 40,-16.75 9.54,22.42L176,100.79l27.58,64.87 0.14,0.34a28.56,28.56 0,0 1,1.24 3.65A29.16,29.16 0,0 1,187.92 204.08Z"
android:fillColor="#ffda3e"
android:fillType="evenOdd" />
<path
android:pathData="M259.45,46l-0.77,13.75 -10.63,-0.57L245.84,99l-0.13,2.29h-0.16A16.6,16.6 0,1 1,232.87 82l1.32,-23.6h0l0.39,-6.87 0.38,-6.88Z"
android:fillColor="#ffcf3b"
android:fillType="evenOdd" />
<path
android:pathData="M205.22,169.54l-0.26,0.11a28.56,28.56 0,0 0,-1.24 -3.65Z"
android:fillColor="#e7b720"
android:fillType="evenOdd" />
<path
android:pathData="M95.05,247.69m-7.01,0a7.01,7.01 0,1 1,14.02 0a7.01,7.01 0,1 1,-14.02 0"
android:fillColor="#231f20" />
<path
android:pathData="M242.42,343.4m-7.01,0a7.01,7.01 0,1 1,14.02 0a7.01,7.01 0,1 1,-14.02 0"
android:fillColor="#231f20" />
</vector>
@@ -1,42 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="m256,13c134.21,0 243,108.79 243,243c0,134.21 -108.79,243 -243,243c-134.21,0 -243,-108.79 -243,-243c0,-134.21 108.79,-243 243,-243z"
android:fillColor="#3f7aff" />
<path
android:pathData="m404.3,630.55l-93.18,24.97c-59.46,15.93 -120.58,-19.35 -136.51,-78.82l-18.52,-69.12c-6.81,-25.42 8.28,-51.56 33.7,-58.37l216.44,-58c25.42,-6.81 51.56,8.28 58.37,33.7l18.52,69.12c15.93,59.46 -19.35,120.58 -78.82,136.51zM255.69,692.33l-1.04,-0.28c-12.36,-3.27 -19.67,-15.89 -16.33,-28.19l13.54,-49.96c3.33,-12.3 16.05,-19.62 28.41,-16.35l1.04,0.28c12.36,3.27 19.67,15.89 16.33,28.19l-13.54,49.96c-3.33,12.3 -16.05,19.62 -28.41,16.35zM436.6,634.7l-36.62,-36.73c-9.01,-9.04 -9.03,-23.69 -0.04,-32.71l0.76,-0.76c9,-9.02 23.6,-9.01 32.61,0.04l36.62,36.73c9.01,9.04 9.03,23.69 0.04,32.71l-0.76,0.76c-9,9.02 -23.6,9.01 -32.61,-0.04z"
android:fillColor="#41eb8e" />
<path
android:pathData="m229.33,167.8c88.82,-23.8 180.11,28.91 203.91,117.73l1.04,3.87c10.01,37.34 -12.15,75.73 -49.5,85.73l-186.4,49.95c-37.34,10.01 -75.73,-12.15 -85.73,-49.5l-1.04,-3.87c-23.8,-88.82 28.91,-180.11 117.73,-203.91zM335.69,116.4l1.09,0.29c12.88,3.41 20.51,16.57 17.03,29.39l-14.12,52.08c-3.48,12.82 -16.74,20.45 -29.62,17.04l-1.09,-0.29c-12.88,-3.41 -20.51,-16.57 -17.03,-29.39l14.12,-52.08c3.48,-12.82 16.74,-20.45 29.62,-17.04zM147.08,176.49l38.17,38.29c9.4,9.43 9.41,24.7 0.04,34.1l-0.79,0.79c-9.38,9.41 -24.6,9.39 -34,-0.04l-38.17,-38.29c-9.4,-9.43 -9.41,-24.7 -0.04,-34.1l0.79,-0.79c9.38,-9.41 24.6,-9.39 34,0.04z"
android:fillColor="#41eb8e" />
<path
android:pathData="m171.1,170.45c14.47,-6.06 31.14,0.64 37.24,14.97c6.09,14.33 -0.7,30.87 -15.17,36.93c-14.47,6.06 -31.14,-0.64 -37.24,-14.97c-6.09,-14.33 0.7,-30.87 15.17,-36.93zM155.24,121.04l21.83,-9.15l32.8,77.13l-21.83,9.15l-32.8,-77.13zM159.84,131.85l-9.2,-21.63l38.57,-16.16l9.2,21.63l-38.57,16.16zM233.88,104.24c8.87,0.47 15.66,7.98 15.17,16.76c-0.49,8.79 -8.07,15.53 -16.94,15.06c-8.87,-0.47 -15.66,-7.98 -15.17,-16.76c0.49,-8.79 8.07,-15.53 16.94,-15.06zM238.18,75.21l13.38,0.71l-2.63,47.3l-13.38,-0.71l2.63,-47.3zM237.81,81.84l0.74,-13.26l23.64,1.25l-0.74,13.26l-23.64,-1.25z"
android:fillColor="#ffcf3b" />
<path
android:pathData="m199.55,259.53c14.4,-3.86 29.2,4.67 33.06,19.06c3.85,14.38 -4.7,29.17 -19.1,33.03c-14.4,3.86 -29.2,-4.67 -33.06,-19.06c-3.85,-14.38 4.7,-29.17 19.1,-33.03z"
android:strokeAlpha="0.8"
android:fillColor="#1f6a50"
android:fillAlpha="0.8" />
<path
android:pathData="m301.94,232.09c14.4,-3.86 29.2,4.67 33.06,19.06c3.85,14.38 -4.7,29.17 -19.1,33.03c-14.4,3.86 -29.2,-4.67 -33.06,-19.06c-3.85,-14.38 4.7,-29.17 19.1,-33.03z"
android:strokeAlpha="0.8"
android:fillColor="#1f6a50"
android:fillAlpha="0.8" />
<path
android:pathData="m281.58,328.54c26.25,15.27 30.16,38.74 18.87,59.03l0.01,0.01l-40.26,70.19c-2.22,3.83 -7.1,5.13 -10.91,2.92l-12.35,-7.18c-3.81,-2.21 -5.1,-7.11 -2.88,-10.93l20.08,-35.15c-4.89,-1.04 -9.74,-2.87 -14.34,-5.55c-14.36,-8.35 -22.78,-22.8 -23.43,-37.43l21.17,-36.56c13.47,-6.97 30.83,-7.03 44.05,0.66z"
android:fillColor="#f9f9f9"
android:fillType="evenOdd" />
<path
android:pathData="m248.73,352.49c2.77,1.61 3.7,5.18 2.08,7.96c-1.62,2.79 -5.17,3.75 -7.93,2.14c-2.77,-1.61 -3.7,-5.18 -2.08,-7.96c1.62,-2.79 5.17,-3.75 7.93,-2.14z"
android:fillColor="#272727" />
<path
android:pathData="m131.01,300.65c-2.65,4.63 -5.93,8.64 -9.64,11.98l35.02,20.46c3.84,2.21 5.17,7.1 2.98,10.93l-7.1,12.39c-2.19,3.82 -7.08,5.13 -10.91,2.92l-70.08,-40.76l0.01,-0.01c-19.95,-11.96 -28.34,-34.28 -13.25,-60.64c7.6,-13.27 22.66,-21.9 37.83,-22.58l36.67,21.14c6.77,13.02 6.72,29.75 -1.54,44.17z"
android:fillColor="#f9f9f9"
android:fillType="evenOdd" />
<path
android:pathData="m106.46,260.17c2.8,1.61 3.78,5.17 2.19,7.95c-1.59,2.78 -5.15,3.72 -7.95,2.11c-2.8,-1.61 -3.78,-5.17 -2.19,-7.95c1.59,-2.78 5.15,-3.72 7.95,-2.11z"
android:fillColor="#272727" />
</vector>
@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="40dp"
android:drawable="@drawable/splash_graphic"
android:left="40dp"
android:right="40dp"
android:top="40dp" />
android:width="48dp"
android:height="48dp"
android:drawable="@mipmap/ic_launcher"
android:gravity="center" />
</layer-list>
@@ -1,34 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_label"
android:id="@+id/user_time"
style="@style/UserTimeStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="12:23" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="match_parent"
android:layout_marginTop="32dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/overview_nomaindevice_label"
app:layout_constraintBottom_toTopOf="@id/permission_description"
app:layout_constraintBottom_toTopOf="@id/body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_description"
android:id="@+id/body"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="32dp"
android:text="@string/overview_nomaindevice_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/permission_label" />
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -7,14 +7,23 @@
android:layout_margin="8dp"
tools:context=".wear.ui.MainActivity">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/user_time"
style="@style/UserTimeStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="12:23" />
<ImageView
android:id="@+id/device_icon"
android:layout_width="16dp"
android:layout_height="16dp"
app:layout_constraintBottom_toTopOf="@+id/name"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="2dp"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/devic_airpods_gen1_both" />
@@ -36,15 +45,15 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:gravity="center"
app:lineHeight="14sp"
android:layout_marginBottom="4dp"
android:gravity="center"
android:lines="2"
android:maxLines="2"
app:layout_constraintBottom_toTopOf="@id/barrier_top"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"
app:lineHeight="14sp"
tools:text="Music Active, Case Closed" />
<androidx.constraintlayout.widget.Barrier
@@ -267,7 +276,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/last_seen"
style="@style/TextAppearance.Material3.BodySmall"
style="@style/TextAppearance.Material3.LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
@@ -7,10 +7,21 @@
android:layout_margin="8dp"
tools:context=".wear.ui.MainActivity">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/user_time"
style="@style/UserTimeStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="12:23" />
<ImageView
android:id="@+id/device_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_width="16dp"
android:layout_height="16dp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
+6
View File
@@ -15,4 +15,10 @@
<item name="android:gravity">center</item>
<item name="android:ellipsize">end</item>
</style>
<style name="UserTimeStyle" parent="TextAppearance.Material3.LabelMedium">
<item name="android:singleLine">true</item>
<item name="android:gravity">center</item>
<item name="android:ellipsize">marquee</item>
</style>
</resources>
+3 -2
View File
@@ -29,9 +29,10 @@
<item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
</style>
<style name="AppThemeSplash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">#3f7aff</item>
<style name="AppThemeSplash" parent="Theme.SplashScreen.IconBackground">
<item name="windowSplashScreenBackground">#000000</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_screen</item>
<item name="windowSplashScreenIconBackgroundColor">#000000</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
+8 -5
View File
@@ -8,8 +8,8 @@ apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin")
android {
compileSdk = ProjectConfig.compileSdk
namespace = "${ProjectConfig.packageName}"
defaultConfig {
applicationId = ProjectConfig.packageName
@@ -100,12 +100,12 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
@@ -141,12 +141,15 @@ dependencies {
addBaseAndroid()
addBaseAndroidUi()
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
addNavigation()
addBaseWorkManager()
addTesting()
"gplayImplementation"("com.android.billingclient:billing:4.0.0")
"gplayImplementation"("com.android.billingclient:billing:5.1.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:5.1.0")
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
@@ -32,7 +32,7 @@ class UpgradeControlFoss @Inject constructor(
override fun launchBillingFlow(activity: Activity) {
MaterialAlertDialogBuilder(activity).apply {
setIcon(R.drawable.ic_heart)
setIcon(eu.darken.capod.common.R.drawable.ic_heart)
setTitle(R.string.upgrade_capod_label)
setMessage(R.string.upgrade_capod_description)
setPositiveButton(R.string.foss_upgrade_donate_label) { _, _ ->
@@ -68,6 +68,6 @@ class UpgradeControlFoss @Inject constructor(
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
}
override fun getSponsorUrl(): String? = "https://github.com/sponsors/d4rken"
override fun getSponsorUrl(): String = "https://github.com/sponsors/d4rken"
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">दान करना</string>
<string name="foss_upgrade_donate_label">दान करें</string>
<string name="foss_upgrade_alreadydonated_label">मैंने पहले ही दान कर दिया है</string>
<string name="foss_upgrade_no_money_label">मैं अपना सारा पैसा AirPods पर खर्च करता हूं</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Já doei</string>
<string name="foss_upgrade_no_money_label">Gastei todo o meu dinheiro em AirPods</string>
</resources>
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Bağış</string>
<string name="foss_upgrade_alreadydonated_label">Ben zaten bağışladım</string>
<string name="foss_upgrade_no_money_label">Tüm paramı AirPod\'lara harcıyorum</string>
<string name="foss_upgrade_donate_label">Bağış yap</string>
<string name="foss_upgrade_alreadydonated_label">Zaten bağış yaptım</string>
<string name="foss_upgrade_no_money_label">Tüm paramı AirPods\'a harcıyorum</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">資助</string>
<string name="foss_upgrade_alreadydonated_label">我已經資助了</string>
<string name="foss_upgrade_no_money_label">我已經為 AirPods 傾家蕩產了</string>
</resources>
@@ -1,13 +0,0 @@
package eu.darken.capod.common.upgrade.core
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
import eu.darken.capod.common.upgrade.core.data.Sku
fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
PurchasedSku(Sku(it), this)
}
private val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Extensions")
@@ -49,7 +49,7 @@ class UpgradeRepoGplay @Inject constructor(
lastProStateAt = now
Info(billingData = data)
}
(now - lastProStateAt) < 6 * 60 * 1000L -> { // 6 hours
(now - lastProStateAt) < 6 * 60 * 60 * 1000L -> { // 6 hours
log(TAG, VERBOSE) { "We are not pro, but were recently, did GPlay try annoy us again?" }
Info(gracePeriod = true, billingData = null)
}
@@ -62,7 +62,7 @@ class UpgradeRepoGplay @Inject constructor(
// Ignore Google Play errors if the last pro state was recent
val now = System.currentTimeMillis()
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, error=$it" }
if ((now - lastProStateAt) < 6 * 60 * 60 * 1000L) { // 6 hours
if ((now - lastProStateAt) < 24 * 60 * 60 * 1000L) { // 24 hours
log(TAG, VERBOSE) { "We are not pro, but were recently, and just and an error, what is GPlay doing???" }
emit(Info(gracePeriod = true, billingData = null))
} else {
@@ -73,7 +73,7 @@ class UpgradeRepoGplay @Inject constructor(
override fun launchBillingFlow(activity: Activity) {
MaterialAlertDialogBuilder(activity).apply {
setIcon(R.drawable.ic_heart)
setIcon(eu.darken.capod.common.R.drawable.ic_heart)
setTitle(R.string.upgrade_capod_label)
setMessage(R.string.upgrade_capod_description)
setPositiveButton(R.string.general_upgrade_action) { _, _ ->
@@ -123,7 +123,7 @@ class UpgradeRepoGplay @Inject constructor(
get() = UpgradeRepo.Type.GPLAY
override val isPro: Boolean
get() = billingData?.getProSku() != null
get() = billingData?.getProSku() != null || gracePeriod
override val upgradedAt: Instant?
get() = billingData
@@ -2,6 +2,7 @@ package eu.darken.capod.common.upgrade.core.client
import android.app.Activity
import com.android.billingclient.api.*
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
@@ -38,7 +39,7 @@ data class BillingClientConnection(
if (!result.isSuccess) {
log(TAG, WARN) { "queryPurchases() failed" }
throw BillingClientException(result)
throw BillingResultException(result)
} else {
requireNotNull(purchases)
}
@@ -47,34 +48,31 @@ data class BillingClientConnection(
return purchases
}
suspend fun acknowledgePurchase(purchase: Purchase): BillingResult {
suspend fun acknowledgePurchase(purchase: Purchase) {
val ack = AcknowledgePurchaseParams.newBuilder().apply {
setPurchaseToken(purchase.purchaseToken)
}.build()
val ackResult = suspendCoroutine<BillingResult> { continuation ->
val result = suspendCoroutine<BillingResult> { continuation ->
client.acknowledgePurchase(ack) { continuation.resume(it) }
}
log(TAG) {
"acknowledgePurchase(purchase=$purchase): code=${ackResult.responseCode}, message=${ackResult.debugMessage})"
}
if (!ackResult.isSuccess) {
throw BillingClientException(ackResult)
}
log(TAG, INFO) { "acknowledgePurchase($purchase): code=${result.responseCode} (${result.debugMessage})" }
return ackResult
if (!result.isSuccess) throw BillingResultException(result)
}
suspend fun querySku(sku: Sku): Sku.Details {
val skuParams = SkuDetailsParams.newBuilder().apply {
setType(BillingClient.SkuType.INAPP)
setSkusList(listOf(sku.id))
val productDetails = QueryProductDetailsParams.Product.newBuilder().apply {
setProductType(BillingClient.ProductType.INAPP)
setProductId(sku.id)
}.build()
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<SkuDetails>?>> { continuation ->
client.querySkuDetailsAsync(skuParams) { skuResult, skuDetails ->
continuation.resume(skuResult to skuDetails)
val params = QueryProductDetailsParams.newBuilder().setProductList(listOf(productDetails)).build()
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<ProductDetails>?>> { continuation ->
client.queryProductDetailsAsync(params) { result, skuDetails ->
continuation.resume(result to skuDetails)
}
}
@@ -82,7 +80,7 @@ data class BillingClientConnection(
"querySku(sku=$sku): code=${result.responseCode}, debug=${result.debugMessage}), skuDetails=$details"
}
if (!result.isSuccess) throw BillingClientException(result)
if (!result.isSuccess) throw BillingResultException(result)
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
@@ -97,10 +95,16 @@ data class BillingClientConnection(
suspend fun launchBillingFlow(activity: Activity, skuDetails: Sku.Details): BillingResult {
log(TAG) { "launchBillingFlow(activity=$activity, skuDetails=$skuDetails)" }
return client.launchBillingFlow(
activity,
BillingFlowParams.newBuilder().setSkuDetails(skuDetails.details.single()).build()
)
val productParams = BillingFlowParams.ProductDetailsParams.newBuilder().apply {
setProductDetails(skuDetails.details.first())
}.build()
val billingFlowParams = BillingFlowParams.newBuilder().apply {
setProductDetailsParamsList(listOf(productParams))
}.build()
return client.launchBillingFlow(activity, billingFlowParams)
}
companion object {
@@ -14,15 +14,15 @@ import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Singleton
class BillingClientConnectionProvider @Inject constructor(
@@ -48,36 +48,40 @@ class BillingClientConnectionProvider @Inject constructor(
}
}.build()
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
log(TAG, VERBOSE) { "startConnection(...)" }
client.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
log(TAG, VERBOSE) {
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
log(TAG, VERBOSE) { "startConnection(...)" }
client.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
log(TAG, VERBOSE) {
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
}
when (result.responseCode) {
BillingResponseCode.OK -> {
val connection = BillingClientConnection(client, purchasePublisher)
trySendBlocking(connection)
launch {
try {
purchasePublisher.value = connection.queryPurchases()
log(TAG) { "Initial IAP query successful." }
} catch (e: Exception) {
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
}
}
}
else -> {
close(BillingResultException(result))
}
continuation.resume(result)
}
}
override fun onBillingServiceDisconnected() {
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
close(CancellationException("Billing service disconnected"))
}
})
}
val billingClientConnection = when (connectionResult.responseCode) {
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
else -> throw BillingClientException(connectionResult)
}
try {
purchasePublisher.value = billingClientConnection.queryPurchases()
log(TAG) { "Initial IAP query successful." }
} catch (e: Exception) {
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
}
send(billingClientConnection)
override fun onBillingServiceDisconnected() {
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
close(BillingException("Billing service disconnected"))
}
})
log(TAG) { "Awaiting close." }
awaitClose {
@@ -89,26 +93,28 @@ class BillingClientConnectionProvider @Inject constructor(
val connection: Flow<BillingClientConnection> = connectionProvider
.setupCommonEventHandlers(TAG) { "connection" }
.retryWhen { cause, attempt ->
log(TAG) { "Billing client connection error: ${cause.asLog()}" }
if (cause is CancellationException) {
log(TAG) { "BillingClient connection cancelled." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
if (cause !is BillingException) {
log(TAG, WARN) { "Unknown exception type: $cause" }
return@retryWhen false
}
if (cause !is BillingClientException) {
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
return@retryWhen false
} else {
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
}
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
return@retryWhen false
}
log(TAG) { "Will retry BillingClient connection... *sigh*" }
delay(3000 * attempt)
true
@@ -1,11 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import com.android.billingclient.api.BillingResult
class BillingClientException(val result: BillingResult) : Exception() {
override val message: String?
get() = result.debugMessage
override fun toString(): String =
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
}
@@ -4,4 +4,14 @@ import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingResult
internal val BillingResult.isSuccess: Boolean
get() = responseCode == BillingClient.BillingResponseCode.OK
get() = responseCode == BillingClient.BillingResponseCode.OK
internal val BillingResult.isGplayUnavailableTemporary: Boolean
get() = setOf(
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
BillingClient.BillingResponseCode.SERVICE_TIMEOUT
).contains(responseCode)
internal val BillingResult.isGplayUnavailablePermanent: Boolean
get() = responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
@@ -0,0 +1,3 @@
package eu.darken.capod.common.upgrade.core.client
open class BillingException(override val message: String) : Exception()
@@ -0,0 +1,9 @@
package eu.darken.capod.common.upgrade.core.client
import com.android.billingclient.api.BillingResult
class BillingResultException(val result: BillingResult) : BillingException(result.debugMessage) {
override fun toString(): String =
"BillingResultException(code=${result.responseCode}, message=${result.debugMessage})"
}
@@ -7,13 +7,9 @@ import eu.darken.capod.common.error.LocalizedError
class GplayServiceUnavailableException(cause: Throwable) : Exception("Google Play services are unavailable.", cause),
HasLocalizedError {
override fun getLocalizedError(context: Context): LocalizedError {
return LocalizedError(
throwable = this,
label = "Google Play Services Unavailable",
description = context.getString(R.string.upgrades_gplay_unavailable_error)
)
}
override fun getLocalizedError(context: Context): LocalizedError = LocalizedError(
throwable = this,
label = "Google Play Services Unavailable",
description = context.getString(R.string.upgrades_gplay_unavailable_error)
)
}
@@ -1,11 +1,14 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.upgrade.core.toPurchasedSku
data class BillingData(
val purchases: Collection<Purchase>
) {
val purchasedSkus: Collection<PurchasedSku>
get() = purchases.map { it.toPurchasedSku() }.flatten()
private fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
PurchasedSku(Sku(it), this)
}
}
@@ -1,7 +1,6 @@
package eu.darken.capod.common.upgrade.core.data
import android.app.Activity
import com.android.billingclient.api.BillingClient.BillingResponseCode
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.*
@@ -10,9 +9,7 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.replayingShare
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.upgrade.core.client.BillingClientConnectionProvider
import eu.darken.capod.common.upgrade.core.client.BillingClientException
import eu.darken.capod.common.upgrade.core.client.GplayServiceUnavailableException
import eu.darken.capod.common.upgrade.core.client.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -53,37 +50,34 @@ class BillingDataRepo @Inject constructor(
}
.forEach {
log(TAG, INFO) { "Acknowledging purchase: $it" }
try {
client.acknowledgePurchase(it)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to ancknowledge purchase: $it\n${e.asLog()}" }
}
client.acknowledgePurchase(it)
}
}
.setupCommonEventHandlers(TAG) { "connection-acks" }
.retryWhen { cause, attempt ->
log(TAG, ERROR) { "Failed to acknowledge purchase: ${cause.asLog()}" }
if (cause is CancellationException) {
log(TAG) { "Ack was cancelled (appScope?) cancelled." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
return@retryWhen false
}
if (cause !is BillingClientException) {
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
if (cause !is BillingException) {
log(TAG, WARN) { "Unknown exception type: $cause" }
return@retryWhen false
} else {
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
}
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
if (cause is BillingResultException && cause.result.isGplayUnavailablePermanent) {
log(TAG) { "Got BILLING_UNAVAILABLE while trying to ACK purchase." }
return@retryWhen false
}
log(TAG) { "Will retry ACK" }
log(TAG) { "Will retry ACK (attempt=$attempt)" }
delay(3000 * attempt)
true
}
@@ -108,7 +102,7 @@ class BillingDataRepo @Inject constructor(
} catch (e: Exception) {
log(TAG, WARN) { "Failed to start IAP flow:\n${e.asLog()}" }
val ignoredCodes = listOf(3, 6)
if (e !is BillingClientException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
if (e !is BillingResultException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
Bugs.report(TAG, "IAP flow failed for $sku", e)
}
@@ -119,16 +113,14 @@ class BillingDataRepo @Inject constructor(
companion object {
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "DataRepo")
internal fun Throwable.tryMapUserFriendly(): Throwable {
if (this !is BillingClientException) return this
return when (result.responseCode) {
BillingResponseCode.BILLING_UNAVAILABLE,
BillingResponseCode.SERVICE_UNAVAILABLE,
BillingResponseCode.SERVICE_DISCONNECTED,
BillingResponseCode.SERVICE_TIMEOUT -> GplayServiceUnavailableException(this)
else -> this
internal fun Throwable.tryMapUserFriendly(): Throwable = when {
this is BillingResultException && this.result.isGplayUnavailableTemporary -> {
GplayServiceUnavailableException(this)
}
this is BillingResultException && this.result.isGplayUnavailablePermanent -> {
GplayServiceUnavailableException(this)
}
else -> this
}
}
}
@@ -1,12 +1,12 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.SkuDetails
import com.android.billingclient.api.ProductDetails
data class Sku(
val id: String
) {
data class Details(
val sku: Sku,
val details: Collection<SkuDetails>,
val details: Collection<ProductDetails>,
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="upgrades_gplay_unavailable_error">Google Play सेवाएं अनुपलब्ध हैं।</string>
<string name="upgrades_no_purchases_found_check_account">कोई खरीदारी नहीं मिली. क्या आप सही खाते का उपयोग कर रहे हैं?</string>
<string name="upgrades_no_purchases_found_check_account">कोई खरीदारी नहीं मिली, क्या आप सही खाते का उपयोग कर रहे हैं?</string>
</resources>
+4 -1
View File
@@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="upgrades_gplay_unavailable_error">Os serviços do Google Play estão indisponíveis.</string>
<string name="upgrades_no_purchases_found_check_account">Nenhuma compra encontrada. Está a usar a conta correta?</string>
</resources>
+4 -1
View File
@@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="upgrades_gplay_unavailable_error">Google Play 服務無法使用。</string>
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
</resources>
+1 -1
View File
@@ -1,5 +1,5 @@
<?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>
</resources>
@@ -6,7 +6,6 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import java.util.concurrent.locks.ReentrantLock
@@ -41,7 +40,7 @@ class ClipboardHelper @Inject constructor(
}
fun copyToClipboard(text: String) {
val clip = ClipData.newPlainText(context.getString(R.string.app_name), text)
val clip = ClipData.newPlainText(context.getString(eu.darken.capod.common.R.string.app_name), text)
clipboard.setPrimaryClip(clip)
}
@@ -73,7 +73,7 @@ class RecorderService : Service2() {
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_baseline_bug_report_24)
.setContentText("Idle")
.setContentTitle(getString(R.string.app_name))
.setContentTitle(getString(eu.darken.capod.common.R.string.app_name))
.addAction(NotificationCompat.Action.Builder(0, getString(R.string.general_done_action), stopPi).build())
startForeground(NOTIFICATION_ID, builder.build())
@@ -115,18 +115,18 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
val baseTitle = when (info.type) {
UpgradeRepo.Type.GPLAY -> {
if (info.isPro) {
getString(R.string.app_name_pro)
getString(eu.darken.capod.common.R.string.app_name_pro)
} else {
gplay.isVisible = true
getString(R.string.app_name)
getString(eu.darken.capod.common.R.string.app_name)
}
}
UpgradeRepo.Type.FOSS -> {
if (info.isPro) {
getString(R.string.app_name_foss)
getString(eu.darken.capod.common.R.string.app_name_foss)
} else {
donate.isVisible = true
getString(R.string.app_name)
getString(eu.darken.capod.common.R.string.app_name)
}
}
}.split(" ".toRegex())
@@ -141,7 +141,7 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
}
builder.append(colorString(requireContext(), color, baseTitle[1]))
} else {
getString(R.string.app_name)
getString(eu.darken.capod.common.R.string.app_name)
}
}
vm.launchUpgradeFlow.observe2 {
@@ -123,7 +123,9 @@ class OverviewFragmentVM @Inject constructor(
if (!isBluetoothEnabled) {
items.add(0, BluetoothDisabledVH.Item)
} else if (mainPod == null) {
items.add(0, MissingMainDeviceVH.Item)
items.add(0, MissingMainDeviceVH.Item {
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navVia(this)
})
}
}
@@ -21,10 +21,12 @@ class MissingMainDeviceVH(parent: ViewGroup) :
item: Item,
payloads: List<Any>
) -> Unit = binding(payload = true) { item ->
troubleshootAction.setOnClickListener { item.onTroubleShoot() }
}
object Item : OverviewAdapter.Item {
data class Item(
val onTroubleShoot: () -> Unit,
) : OverviewAdapter.Item {
override val stableId: Long = Item::class.hashCode().toLong()
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)
@@ -7,9 +7,21 @@ import androidx.core.view.isInvisible
import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasDualMicrophone
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.HasPodStyle
import eu.darken.capod.pods.core.HasStateDetection
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.firstSeenFormatted
import eu.darken.capod.pods.core.getBatteryDrawable
import eu.darken.capod.pods.core.getBatteryLevelCase
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
import eu.darken.capod.pods.core.getBatteryLevelRightPod
import eu.darken.capod.pods.core.lastSeenFormatted
import java.time.Duration
import java.time.Instant
@@ -42,8 +54,10 @@ class DualPodsCardVH(parent: ViewGroup) :
podLeftIcon.setImageResource(device.leftPodIcon)
podRightIcon.setImageResource(device.rightPodIcon)
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
lastSeen.text =
context.getString(eu.darken.capod.common.R.string.last_seen_x, device.lastSeenFormatted(item.now))
firstSeen.text =
context.getString(eu.darken.capod.common.R.string.first_seen_x, device.firstSeenFormatted(item.now))
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
reception.text = item.getReceptionText()
@@ -131,9 +145,9 @@ class DualPodsCardVH(parent: ViewGroup) :
device.apply {
if (this is DualApplePods) {
podCaseLidLabel.text = when (caseLidState) {
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
else -> context.getString(R.string.pods_case_unknown_state)
LidState.OPEN -> context.getString(eu.darken.capod.common.R.string.pods_case_status_open_label)
LidState.CLOSED -> context.getString(eu.darken.capod.common.R.string.pods_case_status_closed_label)
else -> context.getString(eu.darken.capod.common.R.string.pods_case_unknown_state)
}
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
@@ -3,7 +3,6 @@ package eu.darken.capod.main.ui.overview.cards.pods
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
import eu.darken.capod.R
import eu.darken.capod.common.lists.BindableVH
import eu.darken.capod.common.lists.differ.DifferItem
import eu.darken.capod.common.lists.modular.ModularAdapter
@@ -19,7 +18,7 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
fun Item.getReceptionText(): String = device.getSignalQuality(context)
.let { if (showDebug) "$it ${device.seenCounter}" else it }
.let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it }
.let { if (isMainPod) "$it\n(${getString(eu.darken.capod.common.R.string.pods_yours)})" else it }
interface Item : OverviewAdapter.Item {
@@ -32,8 +32,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
reception.text = item.getReceptionText()
details.text = when (item.device) {
is ApplePods -> getString(R.string.pods_unknown_contact_dev)
else -> getString(R.string.pods_unknown_label)
is ApplePods -> getString(eu.darken.capod.common.R.string.pods_unknown_contact_dev)
else -> getString(eu.darken.capod.common.R.string.pods_unknown_label)
}
rawdata.text = device.rawDataHex.joinToString("\n")
@@ -21,7 +21,20 @@ import eu.darken.capod.common.upgrade.isPro
import eu.darken.capod.main.ui.MainActivity
import eu.darken.capod.monitor.core.PodDeviceCache
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.PodFactory
import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.getBatteryDrawable
import eu.darken.capod.pods.core.getBatteryLevelCase
import eu.darken.capod.pods.core.getBatteryLevelHeadset
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
import eu.darken.capod.pods.core.getBatteryLevelRightPod
import finish2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@@ -39,7 +52,7 @@ class WidgetProvider : AppWidgetProvider() {
private fun executeAsync(
tag: String,
timeout: Duration = Duration.ofSeconds(8),
timeout: Duration = Duration.ofSeconds(7),
block: suspend () -> Unit
) {
val start = System.currentTimeMillis()
@@ -52,7 +65,7 @@ class WidgetProvider : AppWidgetProvider() {
} catch (e: Exception) {
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
} finally {
asyncBarrier.finish()
asyncBarrier.finish2()
val stop = System.currentTimeMillis()
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
}
@@ -134,7 +147,7 @@ class WidgetProvider : AppWidgetProvider() {
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
setTextViewText(R.id.primary, context.getString(R.string.pods_unknown_label))
setTextViewText(R.id.primary, context.getString(eu.darken.capod.common.R.string.pods_unknown_label))
}
private fun createNoDeviceLayout(
@@ -150,7 +163,7 @@ class WidgetProvider : AppWidgetProvider() {
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
setTextViewText(R.id.primary, context.getString(R.string.overview_nomaindevice_label))
setTextViewText(R.id.primary, context.getString(eu.darken.capod.common.R.string.overview_nomaindevice_label))
}
private fun createDualPodLayout(
@@ -5,10 +5,10 @@ import android.bluetooth.BluetoothDevice
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.hilt.EntryPoints
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.Bugs
@@ -23,7 +23,6 @@ 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.main.ui.widget.WidgetManager
import eu.darken.capod.monitor.core.MonitorComponent
import eu.darken.capod.monitor.core.MonitorCoroutineScope
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.monitor.ui.MonitorNotifications
@@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.*
class MonitorWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
monitorComponentBuilder: MonitorComponent.Builder,
private val dispatcherProvider: DispatcherProvider,
private val monitorNotifications: MonitorNotifications,
private val notificationManager: NotificationManager,
@@ -55,13 +53,6 @@ class MonitorWorker @AssistedInject constructor(
) : CoroutineWorker(context, params) {
private val workerScope = MonitorCoroutineScope()
private val monitorComponent = monitorComponentBuilder
.coroutineScope(workerScope)
.build()
private val entryPoint by lazy {
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
}
private var finishedWithError = false
@@ -69,10 +60,16 @@ class MonitorWorker @AssistedInject constructor(
log(TAG, VERBOSE) { "init(): workerId=$id" }
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return monitorNotifications.getForegroundInfo(null)
}
override suspend fun doWork(): Result = try {
val start = System.currentTimeMillis()
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
setForeground(monitorNotifications.getForegroundInfo(null))
doDoWork()
val duration = System.currentTimeMillis() - start
@@ -101,7 +98,6 @@ class MonitorWorker @AssistedInject constructor(
val monitorJob = podMonitor.mainDevice
.setupCommonEventHandlers(TAG) { "PodMonitor" }
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
.distinctUntilChanged()
.throttleLatest(1000)
.onEach { currentDevice ->
@@ -1,9 +0,0 @@
package eu.darken.capod.monitor.core.worker
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import eu.darken.capod.monitor.core.MonitorComponent
@InstallIn(MonitorComponent::class)
@EntryPoint
interface MonitorWorkerEntryPoint
@@ -20,6 +20,8 @@ import eu.darken.capod.common.hasApiLevel
import eu.darken.capod.common.notifications.PendingIntentCompat
import eu.darken.capod.main.ui.MainActivity
import eu.darken.capod.pods.core.PodDevice
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
@@ -29,6 +31,7 @@ class MonitorNotifications @Inject constructor(
private val notificationViewFactory: MonitorNotificationViewFactory
) {
private val builderLock = Mutex()
private val builder: NotificationCompat.Builder
init {
@@ -50,20 +53,20 @@ class MonitorNotifications @Inject constructor(
setChannelId(NOTIFICATION_CHANNEL_ID)
setContentIntent(openPi)
priority = NotificationCompat.PRIORITY_LOW
setSmallIcon(R.drawable.devic_earbuds_generic_both)
setSmallIcon(eu.darken.capod.common.R.drawable.devic_earbuds_generic_both)
setOngoing(true)
setContentTitle(context.getString(R.string.app_name))
setContentTitle(context.getString(eu.darken.capod.common.R.string.app_name))
}
}
fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
private fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
if (device == null) {
return builder.apply {
setCustomContentView(null)
setStyle(NotificationCompat.BigTextStyle())
setContentTitle(context.getString(R.string.pods_none_label_short))
setSubText(context.getString(R.string.app_name))
setSmallIcon(R.drawable.devic_earbuds_generic_both)
setContentTitle(context.getString(eu.darken.capod.common.R.string.pods_none_label_short))
setSubText(context.getString(eu.darken.capod.common.R.string.app_name))
setSmallIcon(eu.darken.capod.common.R.drawable.devic_earbuds_generic_both)
}
}
@@ -76,9 +79,13 @@ class MonitorNotifications @Inject constructor(
}
}
fun getNotification(podDevice: PodDevice?): Notification = getBuilder(podDevice).build()
suspend fun getNotification(podDevice: PodDevice?): Notification = builderLock.withLock {
getBuilder(podDevice).build()
}
fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = getBuilder(podDevice).toForegroundInfo()
suspend fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = builderLock.withLock {
getBuilder(podDevice).toForegroundInfo()
}
@SuppressLint("InlinedApi")
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
@@ -0,0 +1,80 @@
package eu.darken.capod.troubleshooter.ui
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.navigation.popBackStack
import eu.darken.capod.common.uix.Fragment3
import eu.darken.capod.common.viewbinding.viewBinding
import eu.darken.capod.databinding.TroubleshooterFragmentBinding
import eu.darken.capod.troubleshooter.ui.TroubleShooterFragmentVM.BleState
import javax.inject.Inject
@AndroidEntryPoint
class TroubleShooterFragment : Fragment3(R.layout.troubleshooter_fragment) {
override val vm: TroubleShooterFragmentVM by viewModels()
override val ui: TroubleshooterFragmentBinding by viewBinding()
@Inject lateinit var webpageTool: WebpageTool
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ui.toolbar.apply {
setupWithNavController(findNavController())
}
ui.bleIntroStartAction.setOnClickListener { vm.troubleShootBle() }
vm.bleState.observe2(ui) { state ->
bleIntroContainer.isVisible = state is BleState.Intro
bleProcessContainer.isVisible = state is BleState.Working
bleResultContainer.isVisible = state is BleState.Result
when (state) {
is BleState.Intro -> {}
is BleState.Working -> {
bleProcessHistory.apply {
text = ""
state.allSteps.forEachIndexed { index, s -> append("#$index: $s\n") }
}
}
is BleState.Result -> {
bleResultHistory.apply {
text = ""
state.history.forEachIndexed { index, s -> append("#$index: $s\n") }
}
when (state) {
is BleState.Result.Success -> {
bleResultAction.isVisible = false
bleResultTitle.text = getString(R.string.troubleshooter_ble_result_success_title)
bleResultBody.text = getString(R.string.troubleshooter_ble_result_success_body)
bleResultAction.text = getString(R.string.general_close_action)
bleResultAction.setOnClickListener { popBackStack() }
}
is BleState.Result.Failure -> {
bleResultTitle.text = getString(R.string.troubleshooter_ble_result_failure_title)
bleResultBody.text = when (state.failureType) {
BleState.Result.Failure.Type.PHONE -> getString(R.string.troubleshooter_ble_result_failure_phone_body)
BleState.Result.Failure.Type.HEADPHONES -> getString(R.string.troubleshooter_ble_result_failure_phone_headphones)
}
bleResultAction.text = getString(R.string.general_check_action)
bleResultAction.setOnClickListener { vm.troubleShootBle() }
}
}
}
}
}
super.onViewCreated(view, savedInstanceState)
}
}
@@ -0,0 +1,269 @@
package eu.darken.capod.troubleshooter.ui
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.uix.ViewModel3
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.unknown.UnknownDevice
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withTimeoutOrNull
import javax.inject.Inject
@HiltViewModel
class TroubleShooterFragmentVM @Inject constructor(
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
private val dispatcherProvider: DispatcherProvider,
private val generalSettings: GeneralSettings,
private val podMonitor: PodMonitor,
private val debugSettings: DebugSettings,
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
private val _bleState = MutableStateFlow<BleState>(BleState.Intro())
val bleState = _bleState.asLiveData2()
init {
_bleState
.onEach { log(TAG) { "New BLE State: $it" } }
.launchIn(vmScope)
}
private fun progress(message: String) {
var state = _bleState.value
if (state !is BleState.Working) {
state = BleState.Working("Starting...")
}
_bleState.value = state.nextStep(message)
}
private fun success(message: String) {
var state = _bleState.value
if (state !is BleState.Working) {
state = BleState.Working("Starting...")
}
_bleState.value = state.toSuccess(message)
}
private fun failure(message: String, type: BleState.Result.Failure.Type) {
var state = _bleState.value
if (state !is BleState.Working) {
state = BleState.Working("Starting...")
}
_bleState.value = state.toFailure(message, type)
}
fun troubleShootBle() = launch(context = dispatcherProvider.IO) {
log(TAG) { "troubleShootBle()" }
generalSettings.scannerMode.value = ScannerMode.LOW_LATENCY
run {
progress("Checking for headphones...")
val mainDevice = withTimeoutOrNull(STEP_TIME) {
podMonitor.mainDevice.filterNotNull().firstOrNull()
}
if (mainDevice != null) {
success("Headphones found, nothing to troubleshoot.")
return@launch
} else {
progress("Headphones not detected.\n")
}
}
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<PodDevice> = { 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.value = hardwareFilteringDisabled
generalSettings.isOffloadedBatchingDisabled.value = hardwareBatchingDisabled
generalSettings.useIndirectScanResultCallback.value = indirectCallback
debugSettings.showUnfiltered.value = unfiltered
val start = System.currentTimeMillis()
val devices = withTimeoutOrNull(STEP_TIME) {
podMonitor.devices
.take(10)
.takeWhile { System.currentTimeMillis() - 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.")
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.value = false
generalSettings.isOffloadedBatchingDisabled.value = false
generalSettings.useIndirectScanResultCallback.value = false
debugSettings.showUnfiltered.value = false
return@launch
}
progress("We received at least some BLE data.\n")
run {
progress("Checking for supported headphones.")
if (doScan(false, false, false, false).any { it !is UnknownDevice }) return@run
if (doScan(false, false, true, false).any { it !is UnknownDevice }) return@run
if (doScan(true, true, true, false).any { it !is UnknownDevice }) return@run
if (doScan(true, true, false, false).any { it !is UnknownDevice }) return@run
if (doScan(true, false, true, false).any { it !is UnknownDevice }) return@run
if (doScan(true, false, false, false).any { it !is UnknownDevice }) return@run
if (doScan(false, true, true, false).any { it !is UnknownDevice }) return@run
if (doScan(false, true, false, false).any { it !is UnknownDevice }) return@run
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
generalSettings.isOffloadedFilteringDisabled.value = false
generalSettings.isOffloadedBatchingDisabled.value = false
generalSettings.useIndirectScanResultCallback.value = false
return@launch
}
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) {
podMonitor.mainDevice.filterNotNull().firstOrNull()
}
if (mainDevice != null) {
success("Found your headphones, new BLE settings worked :)!")
return@launch
}
}
progress("Still no headphones detected that count as yours.\n")
run {
progress("Checking all closeby headphones.")
val otherDevices = withTimeoutOrNull(STEP_TIME) {
val start = System.currentTimeMillis()
podMonitor.devices
.take(10)
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
.toList()
.flatten()
.distinctBy { it.address }
} ?: emptyList()
if (otherDevices.isEmpty()) {
failure("No supported headphones found near your device.", BleState.Result.Failure.Type.HEADPHONES)
return@launch
}
progress("Headphones found nearby, but not detected as yours. Resetting filters.\n")
generalSettings.mainDeviceModel.value = PodDevice.Model.UNKNOWN
generalSettings.mainDeviceAddress.value = null
generalSettings.minimumSignalQuality.value = 0.25f
progress("Setting headphone with strongest signal as yours.")
generalSettings.mainDeviceModel.value = otherDevices.maxBy { it.signalQuality }.model
val mainDevice = withTimeoutOrNull(STEP_TIME) {
podMonitor.mainDevice.filterNotNull().firstOrNull()
}
if (mainDevice != null) {
generalSettings.mainDeviceModel.value = mainDevice.model
generalSettings.scannerMode.value = ScannerMode.BALANCED
success("Success! Detected your headphones.")
return@launch
}
failure("No headphones detected near your device.", BleState.Result.Failure.Type.HEADPHONES)
}
}
sealed class BleState {
class Intro : BleState()
data class Working(
val current: String,
val history: List<String> = emptyList(),
) : BleState() {
val allSteps: List<String>
get() = history + current
fun nextStep(message: String) = this.copy(
current = message,
history = history + current
)
fun toSuccess(message: String) = Result.Success(
history = history + message
)
fun toFailure(message: String, type: Result.Failure.Type) = Result.Failure(
failureType = type,
history = history + message,
)
}
sealed class Result : BleState() {
abstract val history: List<String>
data class Success(
override val history: List<String>,
) : Result()
data class Failure(
val failureType: Type,
override val history: List<String>,
) : Result() {
enum class Type {
PHONE,
HEADPHONES,
;
}
}
}
}
companion object {
const val STEP_TIME = 10 * 1000L // 6 scans per 30 seconds max
val TAG = logTag("TroubleShooter", "Fragment", "VM")
}
}
@@ -32,12 +32,22 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:text="@string/overview_nomaindevice_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/troubleshoot_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/permission_label" />
<com.google.android.material.button.MaterialButton
android:id="@+id/troubleshoot_action"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/troubleshoot_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/permission_description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,186 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/troubleshooter_title" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/ble_card"
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ble_intro_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/troubleshooter_ble_intro_title" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/troubleshooter_ble_intro_body1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ble_intro_start_action"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/troubleshooter_ble_intro_start_action" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/ble_process_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
tools:visibility="visible">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/ble_process_progress"
style="@style/Widget.Material3.CircularProgressIndicator.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/ble_process_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/ble_process_primary" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ble_process_primary"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/troubleshooter_ble_process_title"
app:layout_constraintBottom_toTopOf="@id/ble_process_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ble_process_progress"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ble_process_secondary"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/troubleshooter_ble_process_subtile"
app:layout_constraintBottom_toTopOf="@id/ble_process_history"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/ble_process_primary"
app:layout_constraintTop_toBottomOf="@id/ble_process_primary" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ble_process_history"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ble_process_secondary"
tools:text="Step 1\nStep 2\n\Step 3" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/ble_result_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
tools:visibility="visible">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ble_result_title"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/troubleshooter_ble_result_success_title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ble_result_body"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/troubleshooter_ble_result_success_body" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ble_result_action"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/troubleshooter_ble_result_failure_action" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ble_result_history"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="Step 1\nStep 2\n\Step 3" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -4,17 +4,6 @@
style="@style/PodWidget.Container"
android:orientation="vertical">
<TextView
android:id="@+id/headphones_label"
style="@style/PodWidget.TextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="2dp"
android:textSize="12sp"
android:textStyle="bold"
tools:text="AirPods Max" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -110,4 +99,14 @@
android:src="@drawable/ic_baseline_power_24" />
</LinearLayout>
<TextView
android:id="@+id/headphones_label"
style="@style/PodWidget.TextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="12sp"
android:textStyle="bold"
tools:text="AirPods Max" />
</LinearLayout>
@@ -13,6 +13,9 @@
<action
android:id="@+id/action_overviewFragment_to_settingsFragment"
app:destination="@id/settingsFragment" />
<action
android:id="@+id/action_overviewFragment_to_troubleShooterFragment"
app:destination="@id/troubleShooterFragment" />
</fragment>
<fragment
@@ -20,5 +23,9 @@
android:name="eu.darken.capod.main.ui.settings.SettingsFragment"
android:label="SettingsFragment"
tools:layout="@layout/settings_fragment" />
<fragment
android:id="@+id/troubleShooterFragment"
android:name="eu.darken.capod.troubleshooter.ui.TroubleShooterFragment"
tools:layout="@layout/troubleshooter_fragment" />
</navigation>
+23
View File
@@ -32,6 +32,12 @@
<string name="settings_reaction_label">Рэакцыі</string>
<string name="settings_reaction_description">Рэакцыі на падзеі і дзеянні.</string>
<string name="settings_category_yourdevice_label">Ваша прылада</string>
<string name="settings_category_compatibility_options_title">Параметры сумяшчальнасці</string>
<string name="settings_category_compatibility_options_description">Не чапайце, калі ўсё працуе ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Адключыць апаратную фільтрацыю</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">Не дэлегуйце фільтраванне даных сістэме, замест гэтага накіруйце ўсе даныя ў праграму і дэлегуйце ёй іх фільтраванне.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Адключыць апаратнае пакетаванне</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Не дазваляйце сістэме групаваць сабраныя даныя BLE перад тым, як адправіць іх нам.</string>
<string name="settings_maindevice_address_label">Адрас вашай прылады</string>
<string name="settings_maindevice_address_description">Адрас вашай звязанай прылады. Праграма выкарыстоўвае яго для вызначэння падключэння да вашага тэлефона.</string>
<string name="settings_maindevice_address_none">Няма</string>
@@ -76,4 +82,21 @@
<string name="help_translate_description">Дапамажыце перакласці гэту праграму на вашу мову.</string>
<string name="translators_thanks_title">Перакладчыкі</string>
<string name="translators_thanks_description">Goshaproject(goooosha@gmail.com)</string>
<string name="widget_description">Віджэт паказвае апошні вядомы стан прылады.</string>
<string name="settings_compat_indirectcallback_title">Ускосная дастаўка даных</string>
<string name="settings_compat_indirectcallback_summary">Выкарыстоўваць альтэрнатыўны метад для атрымання даных BLE з сістэмы (трансляцыя замест запытвання даных).</string>
<string name="troubleshooter_title">Сродак выпраўлення непаладак</string>
<string name="troubleshooter_ble_intro_title">Трансляцыя Bluetooth Low Energy</string>
<string name="troubleshooter_ble_intro_body1">AirPods (і падобныя навушнікі) транслююць інфармацыю аб сваім стане з дапамогай тэхналогіі BLE, якая інтэрпрэтуецца прыладамі як рэклама. У некаторых тэлефонах гэтая тэхналогія рэалізавана няправільна. CAPod можа паспрабаваць выправіць гэту памылку, выкарыстоўваючы розныя параметры сумяшчальнасці, пакуль даныя не будуць атрыманыя. Запусціце прайграванне музыкі ў навушніках і размясціце іх побач з тэлефонам, а затым пачніце працэс.</string>
<string name="troubleshooter_ble_intro_start_action">Пачаць выпраўленне непаладак</string>
<string name="troubleshooter_ble_process_title">Ідзе выпраўленне непаладак</string>
<string name="troubleshooter_ble_process_subtile">Кожны крок займае 5–10 секунд</string>
<string name="troubleshooter_ble_result_success_title">Паспяхова</string>
<string name="troubleshooter_ble_result_success_body">Трансляцыі даных BLE прымаюцца CAPod.</string>
<string name="troubleshooter_ble_result_failure_title">Безвынікова</string>
<string name="troubleshooter_ble_result_failure_body">Не ўдалося выправіць непаладку. Не дапамагла ніводная камбінацыя параметраў сумяшчальнасці.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Ваш тэлефон наогул не атрымаў ніякіх даных BLE. Вы можаце паўтарыць гэты тэст у шматлюдным месцы, каб даведацца, ці могуць быць атрыманыя даныя з іншых крыніц (акрамя вашых навушнікаў). Няма атрыманых даных, якія паказваюць на праблему з аперацыйнай сістэмай вашага тэлефона.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Ваш тэлефон атрымаў даныя BLE, але яны паступаюць з прылады, якая не падтрымліваецца. Вашы навушнікі ўключаны? Ці падтрымлівае CAPod вашы навушнікі?</string>
<string name="troubleshooter_ble_result_failure_action">Паспрабаваць яшчэ раз</string>
<string name="troubleshoot_action">Выпраўленне непаладак</string>
</resources>
+23
View File
@@ -32,6 +32,12 @@
<string name="settings_reaction_label">Reaccions</string>
<string name="settings_reaction_description">Reacció davant esdeveniments i comportaments.</string>
<string name="settings_category_yourdevice_label">El vostre dispositiu</string>
<string name="settings_category_compatibility_options_title">Opcions de compatibilitat</string>
<string name="settings_category_compatibility_options_description">No ho toqueu si tot funciona ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Desactiva el filtratge de maquinari</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">No delegueu el filtratge de dades al sistema, obtingueu totes les dades i filtreu-les dins de l\'aplicació.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Desactiva el processament per lots de maquinari</string>
<string name="settings_compat_offloaded_batching_disabled_summary">No deixeu que el grup del sistema reculli dades BLE abans de reenviar-nos-les.</string>
<string name="settings_maindevice_address_label">L\'adreça del vostre dispositiu</string>
<string name="settings_maindevice_address_description">L\'adreça del dispositiu emparellat. L\'aplicació la fa servir per determinar quan està connectat al telèfon.</string>
<string name="settings_maindevice_address_none">Cap</string>
@@ -76,4 +82,21 @@
<string name="help_translate_description">Ajudeu a traduir aquesta aplicació al vostre idioma preferit.</string>
<string name="translators_thanks_title">Traductors</string>
<string name="translators_thanks_description">Jaime Muñoz(jmmartin_5@outlook.com)</string>
<string name="widget_description">Un giny que mostra l\'estat de l\'últim dispositiu conegut.</string>
<string name="settings_compat_indirectcallback_title">Lliurament indirecte de dades</string>
<string name="settings_compat_indirectcallback_summary">Utilitza un mètode alternatiu per rebre dades BLE del sistema (emissió en lloc de devolució de trucada).</string>
<string name="troubleshooter_title">Solucionador de problemes</string>
<string name="troubleshooter_ble_intro_title">Emissions Bluetooth de baixa energia</string>
<string name="troubleshooter_ble_intro_body1">Els AirPods (i auriculars similars) transmeten informació d\'estat mitjançant una tecnologia BLE anomenada «anuncis». Alguns telèfons no implementen aquesta tecnologia correctament. El CAPod pot intentar solucionar-ho provant diferents opcions de compatibilitat fins que es rebin les dades. Inicieu la reproducció de música als vostres auriculars i col·loqueu-los a prop del telèfon i, a continuació, inicieu el procés.</string>
<string name="troubleshooter_ble_intro_start_action">Inicia el solucionador de problemes</string>
<string name="troubleshooter_ble_process_title">Resolució de problemes en curs</string>
<string name="troubleshooter_ble_process_subtile">Cada pas dura entre 5 i 10 segons</string>
<string name="troubleshooter_ble_result_success_title">Correcte</string>
<string name="troubleshooter_ble_result_success_body">El CAPod està rebent emissions d\'anuncis BLE.</string>
<string name="troubleshooter_ble_result_failure_title">Incorrecte</string>
<string name="troubleshooter_ble_result_failure_body">La resolució de problemes ha fallat. No ha funcionat cap combinació d\'opcions de compatibilitat.</string>
<string name="troubleshooter_ble_result_failure_phone_body">El vostre telèfon no ha rebut cap dada BLE. Podeu tornar a provar aquesta prova en una zona plena de gent per veure si es poden rebre fonts de dades (que no siguin els vostres auriculars). No es reben dades que indiquen un problema amb el sistema operatiu del vostre telèfon.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">El vostre telèfon ha rebut dades BLE, però les dades no provenen de cap dispositiu compatible. Teniu els auriculars encesos? El CAPod és compatible amb els vostres auriculars?</string>
<string name="troubleshooter_ble_result_failure_action">Torna-ho a provar</string>
<string name="troubleshoot_action">Resolució de problemes</string>
</resources>
+23
View File
@@ -32,6 +32,12 @@
<string name="settings_reaction_label">Reakce</string>
<string name="settings_reaction_description">Reakce na události a chování.</string>
<string name="settings_category_yourdevice_label">Vaše zařízení</string>
<string name="settings_category_compatibility_options_title">Možnosti kompatibility</string>
<string name="settings_category_compatibility_options_description">Nedotýkejte se, pokud vše funguje ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Vypnout hardwarové filtrování</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">Nesvěřujte filtrování dat systému, ale přijímejte a filtrujte všechna data v aplikaci.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Vypnout hardwarové dávkování</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Nedovolte, aby systém seskupil shromážděná data BLE předtím, než nám je předá.</string>
<string name="settings_maindevice_address_label">Adresa vašeho zařízení</string>
<string name="settings_maindevice_address_description">Adresa spárovaného zařízení. Podle toho aplikace určí, kdy je připojena k telefonu.</string>
<string name="settings_maindevice_address_none">Žádná</string>
@@ -76,4 +82,21 @@
<string name="help_translate_description">Pomozte přeložit tuto aplikaci do svého oblíbeného jazyka.</string>
<string name="translators_thanks_title">Překladatelé</string>
<string name="translators_thanks_description">novas78@xda; woytazzer</string>
<string name="widget_description">Widget zobrazující poslední známý stav zařízení.</string>
<string name="settings_compat_indirectcallback_title">Nepřímé poskytování dat</string>
<string name="settings_compat_indirectcallback_summary">Použijte alternativní metodu pro příjem dat BLE ze systému (vysílání namísto přijímání).</string>
<string name="troubleshooter_title">Řešení problémů</string>
<string name="troubleshooter_ble_intro_title">Vysílání Bluetooth Low Energy</string>
<string name="troubleshooter_ble_intro_body1">Sluchátka AirPods (a podobná sluchátka) vysílají informace o stavu pomocí technologie BLE zvané \"reklama\". Některé telefony tuto technologii neimplementují správně. CAPod se může pokusit o nápravu zkoušením různých možností kompatibility, dokud nebudou data přijata. Spusťte přehrávání hudby ve sluchátkách, přiložte je k telefonu a spusťte proces.</string>
<string name="troubleshooter_ble_intro_start_action">Spustit řešení problémů</string>
<string name="troubleshooter_ble_process_title">Probíhá řešení problémů</string>
<string name="troubleshooter_ble_process_subtile">Každý krok trvá 5-10 sekund</string>
<string name="troubleshooter_ble_result_success_title">Úspěch</string>
<string name="troubleshooter_ble_result_success_body">CAPod přijímá reklamní vysílání BLE.</string>
<string name="troubleshooter_ble_result_failure_title">Neúspěšné</string>
<string name="troubleshooter_ble_result_failure_body">Řešení problémů se nezdařilo. Žádná kombinace možností kompatibility nepomohla.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Telefon nepřijal vůbec žádná data BLE. Tento test můžete zopakovat v přelidněném prostoru, abyste zjistili, zda lze přijímat i jiné zdroje dat (než vaše sluchátka). Nepřijímání dat ukazuje na problém s operačním systémem telefonu.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Telefon přijal data BLE, ale data nepocházejí z žádného podporovaného zařízení. Jsou sluchátka zapnutá? Podporuje CAPod vaše sluchátka?</string>
<string name="troubleshooter_ble_result_failure_action">Zkusit znovu</string>
<string name="troubleshoot_action">Vyřešit problém</string>
</resources>
+23
View File
@@ -32,6 +32,12 @@
<string name="settings_reaction_label">Reaktionen</string>
<string name="settings_reaction_description">Reagieren Sie auf Ereignisse und Verhaltensweisen.</string>
<string name="settings_category_yourdevice_label">Ihr Gerät</string>
<string name="settings_category_compatibility_options_title">Kompatibilitätsoptionen</string>
<string name="settings_category_compatibility_options_description">Nicht anfassen, wenn alles funktioniert ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Deaktivieren Sie die Hardwarefilterung</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">Delegieren Sie die Datenfilterung nicht an das System, sondern erhalten Sie alle Daten und Filter innerhalb der App.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Hardware-Batching deaktivieren</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Lassen Sie nicht zu, dass die Systemgruppe BLE-Daten sammelt, bevor Sie sie an uns weiterleiten.</string>
<string name="settings_maindevice_address_label">Ihre Geräteadresse</string>
<string name="settings_maindevice_address_description">Die Adresse Ihres gekoppelten Geräts. Die App verwendet dies, um festzustellen, wann sie mit Ihrem Telefon verbunden ist.</string>
<string name="settings_maindevice_address_none">Keine</string>
@@ -76,4 +82,21 @@
<string name="help_translate_description">Hilf mit diese App in deine Lieblingssprache zu übersetzen.</string>
<string name="translators_thanks_title">Übersetzer</string>
<string name="translators_thanks_description">Gamechanger181</string>
<string name="widget_description">Ein Widget, das den letzten bekannten Gerätestatus anzeigt.</string>
<string name="settings_compat_indirectcallback_title">Indirekte Datenlieferung</string>
<string name="settings_compat_indirectcallback_summary">Verwenden Sie eine alternative Methode, um BLE-Daten vom System zu erhalten (Broadcast statt Callback).</string>
<string name="troubleshooter_title">Fehlerbehebung</string>
<string name="troubleshooter_ble_intro_title">Bluetooth Low Energy-Sendungen</string>
<string name="troubleshooter_ble_intro_body1">AirPods (und ähnliche Kopfhörer) übertragen Statusinformationen mithilfe einer BLE-Technologie namens „Werbung“. Einige Telefone implementieren diese Technologie nicht richtig. CAPod kann versuchen, dies zu beheben, indem verschiedene Kompatibilitätsoptionen ausprobiert werden, bis Daten empfangen werden. Starten Sie die Musikwiedergabe auf Ihren Kopfhörern und legen Sie sie in die Nähe Ihres Telefons, und starten Sie dann den Vorgang.</string>
<string name="troubleshooter_ble_intro_start_action">Beginnen Sie mit der Fehlerbehebung</string>
<string name="troubleshooter_ble_process_title">Fehlerbehebung läuft</string>
<string name="troubleshooter_ble_process_subtile">Jeder Schritt dauert 510 Sekunden</string>
<string name="troubleshooter_ble_result_success_title">Erfolg</string>
<string name="troubleshooter_ble_result_success_body">BLE-Werbesendungen werden von CAPod empfangen.</string>
<string name="troubleshooter_ble_result_failure_title">Erfolglos</string>
<string name="troubleshooter_ble_result_failure_body">Fehlerbehebung fehlgeschlagen. Keine Kombination von Kompatibilitätsoptionen hat geholfen.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Ihr Telefon hat überhaupt keine BLE-Daten empfangen. Sie können diesen Test in einem überfüllten Bereich wiederholen, um zu sehen, ob Datenquellen (außer Ihren Kopfhörern) empfangen werden können. Es werden keine Daten empfangen, was auf ein Problem mit dem Betriebssystem Ihres Telefons hindeutet.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Ihr Telefon hat BLE-Daten empfangen, aber die Daten stammen von keinem unterstützten Gerät. Sind Ihre Kopfhörer eingeschaltet? Unterstützt CAPod Ihren Kopfhörer?</string>
<string name="troubleshooter_ble_result_failure_action">Versuchen Sie es erneut</string>
<string name="troubleshoot_action">Fehlerbehebung</string>
</resources>
+51 -27
View File
@@ -7,31 +7,37 @@
<string name="general_upgrade_action">Actualizar</string>
<string name="general_check_action">Verificar</string>
<string name="general_close_action">Cerrar</string>
<string name="upgrade_capod_label">Actualitzar CAPod</string>
<string name="upgrade_capod_description">Obtenga funciones adicionales y apoye al desarrollador.</string>
<string name="upgrade_capod_label">Actualizar CAPod</string>
<string name="upgrade_capod_description">Consigue características adicionales y apoya al desarrollador.</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 Bluetooth.</string>
<string name="settings_scanner_mode_label">Modo escáner</string>
<string name="settings_scanner_mode_description">¿El escáner de datos Bluetooth Low Energy debe priorizar el rendimiento o ahorrar energía?</string>
<string name="settings_autopause_label">Pausar automáticamente</string>
<string name="settings_autopause_description">Pausa el audio cuando te quites el dispositivo de la oreja.</string>
<string name="settings_scanner_mode_description">¿El escáner de datos del Bluetooth de baja energía debe priorizar el rendimiento o conservar la batería?</string>
<string name="settings_autopause_label">Pausa automática</string>
<string name="settings_autopause_description">Pausa el audio cuando retires el dispositivo del oído.</string>
<string name="settings_showall_label">Mostrar todos los dispositivos</string>
<string name="settings_showall_description">Muestra los dispositivos de otras personas que están cerca de ti.</string>
<string name="settings_showall_description">Muestra los dispositivos de otras personas que están cerca.</string>
<string name="settings_autopplay_label">Reproducción automática</string>
<string name="settings_autoplay_description">Inicia la reproducción de audio cuando se use el dispositivo.</string>
<string name="settings_autoplay_description">Inicia la reproducción del audio cuando se usa el dispositivo.</string>
<string name="settings_fake_data_label">Información falsa</string>
<string name="settings_fake_data_description">Mostrar datos falsos. Por ejemplo: simular un dispositivo que no existe.</string>
<string name="settings_debug_label">Configuración de depuración</string>
<string name="settings_debug_description">Configuraciones 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 debe tener un dispositivo para ser considerado suyo.</string>
<string name="settings_autoconnect_label">Autoconectar</string>
<string name="settings_fake_data_description">Mostrar los datos falsos, es decir, simular dispositivos que no existen.</string>
<string name="settings_debug_label">Opciones de depuración</string>
<string name="settings_debug_description">Ajustes adicionales para ayudar a solucionar los 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 ser considerado tuyo.</string>
<string name="settings_autoconnect_label">Conectar automáticamente</string>
<string name="settings_autoconnect_description">Si Android no se conecta automáticamente, también podemos preguntarlo. Esto establecerá la configuración del modo de monitor en \'Siempre\'.</string>
<string name="settings_autoconnect_condition_label">Condición de autoconexión</string>
<string name="settings_autoconnect_condition_description">¿Cuándo debemos intentar conectarnos a su dispositivo?</string>
<string name="settings_reaction_label">Reacciones</string>
<string name="settings_reaction_description">Reaccionar a eventos y comportamientos.</string>
<string name="settings_category_yourdevice_label">Tu dispositivo</string>
<string name="settings_category_compatibility_options_title">Compatibilidad</string>
<string name="settings_category_compatibility_options_description">No toques nada si todo funciona ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Desactivar el filtrado por hardware</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">No delegues el filtrado de los datos al sistema, en cambio obtén todos los datos y fíltralos dentro de la aplicación.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Deshabilitar el procesamiento por lotes del hardware</string>
<string name="settings_compat_offloaded_batching_disabled_summary">No permitas que el sistema recopile los datos BLE antes de enviárnoslos.</string>
<string name="settings_maindevice_address_label">La dirección de su dispositivo</string>
<string name="settings_maindevice_address_description">La dirección de su dispositivo emparejado. La aplicación usa esto para determinar cuándo está conectado a su teléfono.</string>
<string name="settings_maindevice_address_none">Ninguno</string>
@@ -44,35 +50,53 @@
<string name="notification_channel_device_status_label">Estado del dispositivo</string>
<string name="debug_debuglog_size_label">Tamaño</string>
<string name="debug_debuglog_size_compressed_label">Tamaño comprimido</string>
<string name="debug_notification_channel_label">Notificaciones depuradas</string>
<string name="debug_notification_channel_label">Notificaciones de la depuración</string>
<string name="debug_debuglog_file_label">Registro guardado</string>
<string name="debug_debuglog_record_action">Grabar registro de depuracion</string>
<string name="settings_support_installid_label">ID de instalación</string>
<string name="settings_support_installid_desc">Los informes automáticos de errores son anónimos. Comparta su ID de instalación si el desarrollador necesita encontrar sus informes de error.</string>
<string name="debug_debuglog_record_action">Registro de depuración</string>
<string name="settings_support_installid_label">ID de la instalación</string>
<string name="settings_support_installid_desc">Los informes automáticos de errores son anónimos. Comparte tu ID de la instalación si el desarrollador necesita encontrar tus informes de errores.</string>
<string name="settings_support_label">Ayuda</string>
<string name="settings_support_description">Si necesita ayuda.</string>
<string name="issue_tracker_label">Rastreador de problemas</string>
<string name="issue_tracker_description">Un rastreador público de problemas para informes de errores y solicitudes de funciones (solo en inglés).</string>
<string name="discord_description">Un lugar para pasar el rato y hacer preguntas.</string>
<string name="settings_support_description">Si necesitas ayuda.</string>
<string name="issue_tracker_label">Gestor de incidencias</string>
<string name="issue_tracker_description">Un gestor de incidencias público para los informes de errores y solicitudes de funciones (solo en inglés).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Un lugar donde pasar el rato y hacer preguntas.</string>
<string name="changelog_label">Registro de cambios</string>
<string name="settings_label">Ajustes</string>
<string name="settings_privacy_policy_label">Política de privacidad</string>
<string name="settings_privacy_policy_desc">Manejo responsable de datos.</string>
<string name="settings_privacy_policy_desc">Tratamiento responsable de los datos.</string>
<string name="settings_licenses_label">Licencias</string>
<string name="settings_category_other_label">Otros</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_acknowledgements_label">Agradecimientos</string>
<string name="settings_debug_autoreports_label">Informes automáticos de errores</string>
<string name="settings_debug_autoreports_description">Informa automáticamente de los problemas. Por ejemplo: detalles sobre un bloqueo 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">Muestra información adicional para solucionar problemas.</string>
<string name="settings_debug_autoreports_description">Informa automáticamente de los problemas, por ejemplo, de los detalles de 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 los problemas.</string>
<string name="settings_blescanner_unfiltered_label">Datos BLE sin filtrar</string>
<string name="settings_blescanner_unfiltered_description">Elimina los filtros del escáner BLE para mostrar todos los datos BLE transmitidos. Útil para agregar soporte para nuevos tipos de auriculares.</string>
<string name="settings_blescanner_unfiltered_description">Elimina cualquier filtro del escáner BLE para mostrar todos los datos BLE emitidos. Ú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">Deshabilita las optimizaciones para mejorar la compatibilidad. Prueba esto si no ves ningún dato.</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>
<string name="help_translate_description">Ayuda a traducir esta aplicación a tu idioma favorito.</string>
<string name="translators_thanks_title">Traductores</string>
<string name="translators_thanks_description">Jaime Muñoz(jmmartin_5@outlook.com)</string>
<string name="widget_description">Un widget que muestra como estaba tu dispositivo.</string>
<string name="settings_compat_indirectcallback_title">Envío indirecto de datos</string>
<string name="settings_compat_indirectcallback_summary">Utiliza un método alternativo para recibir los datos BLE del sistema (emisión en lugar de devolución de llamada).</string>
<string name="troubleshooter_title">Reparador de problemas</string>
<string name="troubleshooter_ble_intro_title">Transmisiones Bluetooth de bajo consumo</string>
<string name="troubleshooter_ble_intro_body1">Los AirPods (y auriculares similares) transmiten información de su estado mediante una tecnología BLE llamada \"anuncios\". Algunos teléfonos no implementan esta tecnología correctamente. CAPod puede intentar arreglar esto probando diferentes opciones de compatibilidad hasta que se reciban los datos. Inicia la reproducción de la música en tus auriculares y colócalos cerca de tu teléfono, luego comienza el proceso.</string>
<string name="troubleshooter_ble_intro_start_action">Empezar a resolver los problemas</string>
<string name="troubleshooter_ble_process_title">Solución de los problemas en curso</string>
<string name="troubleshooter_ble_process_subtile">Cada paso dura entre 5 y 10 segundos</string>
<string name="troubleshooter_ble_result_success_title">Correcto</string>
<string name="troubleshooter_ble_result_success_body">CAPod recibe las emisiones de anuncios BLE.</string>
<string name="troubleshooter_ble_result_failure_title">Incorrecto</string>
<string name="troubleshooter_ble_result_failure_body">La solución de problemas falló. Ninguna combinación de las opciones de compatibilidad ayudó.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Tu teléfono no ha recibido ningún dato BLE. Puedes volver a realizar esta prueba en una zona concurrida para ver si se pueden recibir otras fuentes de datos (distintas de los auriculares). El hecho de que no se reciban los datos apunta a un problema con el sistema operativo del teléfono.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Tu teléfono recibió los datos BLE, pero los datos no proceden de ningún dispositivo compatible. ¿Están encendidos tus auriculares? ¿Es CAPod compatible con tus auriculares?</string>
<string name="troubleshooter_ble_result_failure_action">Vuelve a intentarlo</string>
<string name="troubleshoot_action">Reparador de los problemas</string>
</resources>

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