Compare commits

...
72 Commits
Author SHA1 Message Date
darken 02f68821d2 Release: 2.14.0-rc0 2024-07-12 15:04:28 +02:00
darken 62886f00f1 Show battery values in notification content titles 2024-07-12 14:47:01 +02:00
darken f2eb45d00b Fix app crash on launch if permission are not granted
```java
12:49:29.433 AndroidRuntime                       E  FATAL EXCEPTION: main
                                                     Process: eu.darken.capod, PID: 15164
                                                     java.lang.SecurityException: Starting FGS with type connectedDevice callerApp=ProcessRecord{cb03698 15164:eu.darken.capod/u0a352} targetSDK=34 requires permissions: all of the permissions allOf=true [android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE] any of the permissions allOf=false [android.permission.BLUETOOTH_ADVERTISE, android.permission.BLUETOOTH_CONNECT, android.permission.BLUETOOTH_SCAN, android.permission.CHANGE_NETWORK_STATE, android.permission.CHANGE_WIFI_STATE, android.permission.CHANGE_WIFI_MULTICAST_STATE, android.permission.NFC, android.permission.TRANSMIT_IR, android.permission.UWB_RANGING, USB Device, USB Accessory]
                                                     	at android.os.Parcel.createExceptionOrNull(Parcel.java:3242)
                                                     	at android.os.Parcel.createException(Parcel.java:3226)
                                                     	at android.os.Parcel.readException(Parcel.java:3209)
                                                     	at android.os.Parcel.readException(Parcel.java:3151)
                                                     	at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:7167)
                                                     	at android.app.Service.startForeground(Service.java:863)
                                                     	at androidx.work.impl.foreground.SystemForegroundService$Api31Impl.startForeground(SystemForegroundService.java:194)
                                                     	at androidx.work.impl.foreground.SystemForegroundService$1.run(SystemForegroundService.java:130)
                                                     	at android.os.Handler.handleCallback(Handler.java:959)
                                                     	at android.os.Handler.dispatchMessage(Handler.java:100)
                                                     	at android.os.Looper.loopOnce(Looper.java:232)
                                                     	at android.os.Looper.loop(Looper.java:317)
                                                     	at android.app.ActivityThread.main(ActivityThread.java:8699)
                                                     	at java.lang.reflect.Method.invoke(Native Method)
                                                     	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
                                                     	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
2024-07-12 13:44:57 +02:00
darken 2fca9097c2 Fix up-navigation behavior when doing onboarding
Back button shouldn't take you dashboard -> onboarding or onboarding -> dashboard
2024-07-12 13:44:44 +02:00
Ricky Cheung 051677e7dd Prevent return@apply duplicates
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung fbcd595b45 Add more spacing around the wide variant of dual pods widgets
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung aa38f58f62 setCustomContentView() -> setCustomBigContentView()
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung a3b01a02ee Update all checkbox preferences to SUW switches
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung 8544983984 Update com.google.android.material to 1.12.0
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-07-12 12:18:24 +02:00
Ricky Cheung 07a11799b1 Implement support for different states in notification text
Fixes #198

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-27 08:34:19 +02:00
Kavish Devar 65a7b1933a Add PUSH_NOTIFICATIONS permission to AndroidManifest.xml for wearos (#203)
* Add PUSH_NOTIFICATIONS permissions to AndroidManifest.xml for wearos

Fixes https://github.com/d4rken-org/capod/issues/154

* Fix typo in AndroidManifest.xml for wearos app
2024-06-26 12:41:47 +02:00
Ricky Cheung bbfb735124 Enlarge the icons on wide widgets
* Also move the case details to the centre and separate the text a little

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 7b474fb0e1 Fix up comment
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung c6524d7cee Log layout ID in createDualPodLayout
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 148cdf5109 Add more notes for the wide widgets
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 60a3039415 Annotate layout argument as LayoutRes
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 6f7b6e30c9 Fix status bar icons in light theme
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 08c095eae4 Switch all MaterialComponents for Material3 for Wear
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 9087be6282 Add wide variant of dual pod widget
* Wide variant activates when the widget is larger than 3 columns in length

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung d46fe35eab Make widgets consistent with each other
Going by 2f9185efa7, it appears the label is preferred to be at the bottom

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung 1afb77ee33 Make the status bar transparent
Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
Ricky Cheung a9215fc11c Switch to everything Material3 components
This commit modifies all major elements of the UI for phones to use Material3 elements, including the toolbar.

Signed-off-by: Ricky Cheung <rcheung844@gmail.com>
2024-06-26 12:41:24 +02:00
darken d80a110280 Thank the MagicPods project 2024-04-30 16:10:06 +02:00
darken fdfc7f44fb Update translations 2024-04-30 15:37:18 +02:00
darken 26fbfc08f7 Remove proprietary Google Play info block in FOSS builds 2024-04-05 18:42:11 +02:00
hwlnx 89049ee3ab Update preferences_support.xml 2024-03-11 13:30:22 +01:00
hwlnx 68f81f900b Update strings.xml 2024-03-11 13:29:19 +01:00
darken 513bd891af Release: 2.13.0-rc0 2024-02-25 13:30:10 +01:00
darken 37cd753ab8 Update README 2024-02-25 13:29:13 +01:00
darken 34bf2311b4 Add support for PowerBeats4
Thanks to @docgalaxyblock
2024-02-25 13:29:13 +01:00
darken a86093f617 Update translations 2024-02-25 13:16:19 +01:00
Matthias Urhahn 71f66e6004 Merge pull request #182 from JerretQueen/main
Onboarding typo fixed, dual pods layout realigned.
2024-02-25 13:12:31 +01:00
Matthias Urhahn 2d1ab85e01 Update README.md 2024-02-25 13:11:46 +01:00
Matthias Urhahn f029eb9102 Merge pull request #179 from hwlnx/patch-1
Update strings.xml
2024-02-25 13:06:15 +01:00
Matthias Urhahn 471837ab88 Update gradle-wrapper.properties 2024-02-25 13:05:05 +01:00
Matthias Urhahn 1e80d742cc Update build.gradle.kts 2024-02-25 13:04:38 +01:00
Matthias Urhahn 16b6273938 Update build.gradle.kts 2024-02-25 13:04:08 +01:00
JerretQueen 3407370563 1. Fixed a typo in @string/onboarding_body2. The message now correctly says "implementation" instead of "implemtation".
2. Changed layout of overview_pods_dual_item.xml so that the left and right pod status are properly lined up with each other.
2023-12-30 00:14:24 -06:00
hwlnx 3cda73a36c Update strings.xml 2023-11-19 20:05:42 +01:00
darken b604c423ad Release: 2.12.1-rc0 2023-10-28 19:51:26 +02:00
darken 0c8b121c2c Request FOREGROUND_SERVICE_SPECIAL_USE for debug log service 2023-10-28 19:50:56 +02:00
darken da11961611 Release: 2.12.0-rc0 2023-10-28 18:36:11 +02:00
darken 48b9952fc0 Update translations 2023-10-28 18:21:50 +02:00
darken 99fa99eba5 Update billing client 2023-10-28 18:03:28 +02:00
darken 06e67e04ac Move permission 2023-10-28 17:58:09 +02:00
darken 4a2f7c20a2 Update unit test 2023-10-28 17:58:09 +02:00
darken 82e8769407 Bump dependencies and API targets. 2023-10-28 17:58:09 +02:00
darken ad38702df1 Widget: Center device label 2023-10-28 16:59:52 +02:00
darken 3f98c62a8f Extend test case for AirPods Pro 2 (USB-C)
Closes #164
2023-09-24 12:01:15 +02:00
darken ec7447992f Release: 2.11.1-rc0 2023-09-23 21:44:04 +02:00
Matthias Urhahn 99958e13eb Update translations (#166) 2023-09-23 21:43:24 +02:00
Matthias Urhahn b9fe5d4a14 Update README.md 2023-09-23 21:38:44 +02:00
darken 1443fd5714 Support for Apple AirPods Pro2 with USB-C
See #164
2023-09-23 21:35:30 +02:00
darken 798083b235 Remove link from changelog notes (eggshells) 2023-09-11 23:35:20 +02:00
darken 18a7311089 Release: 2.11.0-rc0 2023-09-11 22:45:47 +02:00
darken 9f49d62f12 Clean up store description and remove WearOS sentences. 2023-09-11 21:09:02 +02:00
darken 0c3c4e7ea1 Update translations 2023-09-11 21:09:02 +02:00
darken 7cdf653842 Remove WearOS part from app description, it's no longer on Google Play. 2023-09-11 21:09:02 +02:00
darken 32d99eada8 Prevent app from crashing if no app to view urls is installed 2023-09-11 20:57:15 +02:00
darken 9e4adb16c6 Hide install, not useful without 3rd party bug tracker 2023-09-11 20:33:06 +02:00
darken 4b5638e4f0 Hide bug reporting setting (but lets keep the setting ID because if we re-add we might use the setting state again). 2023-09-11 20:32:53 +02:00
darken d8fb9cdd30 Remove bugsnag 2023-09-11 20:32:53 +02:00
Matthias Urhahn 3920489dcd Extra onboarding step (#158)
* wip

* Add extra onboarding step
2023-09-11 20:32:41 +02:00
Matthias Urhahn 09d013588d Update PRIVACY_POLICY.md 2023-09-11 01:33:38 +02:00
darken d8bd03c65d Remove WearOS build from Google Play pipeline. 2023-07-16 01:05:04 +02:00
darken 8b2c6b9498 Release: 2.10.0-rc2 2023-07-07 10:13:48 +02:00
darken 0f63d3a0d4 Revert "Don't bump WearOS, not needed due to new track system?"
This reverts commit a321bb733c.
2023-07-07 10:12:56 +02:00
darken 672eb333b0 Update fastlane 2023-07-07 10:12:24 +02:00
darken de609b4d05 Release: 2.10.0-rc1 2023-07-07 09:05:38 +02:00
darken 7e6e9eee36 Try WearOS first as it is more likely to fail. 2023-07-07 09:04:34 +02:00
darken a321bb733c Don't bump WearOS, not needed due to new track system? 2023-07-07 09:04:34 +02:00
darken 490369a3e1 Adjust CI to build and upload to WearOS beta/production tracks 2023-07-07 09:04:34 +02:00
174 changed files with 1864 additions and 738 deletions
+17 -5
View File
@@ -48,7 +48,6 @@ jobs:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
- name: Assemble production APK
if: "!contains(steps.tagger.outputs.tag, '-beta')"
@@ -58,7 +57,6 @@ jobs:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
- name: Create pre-release
if: contains(steps.tagger.outputs.tag, '-beta')
@@ -138,6 +136,14 @@ jobs:
ruby-version: 2.7.6
bundler-cache: true
# - name: Assemble WearOS beta and upload to Google Play
# if: contains(steps.tagger.outputs.tag, '-beta')
# run: bundle exec fastlane beta_wearos
# env:
# STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
- name: Assemble beta and upload to Google Play
if: contains(steps.tagger.outputs.tag, '-beta')
run: bundle exec fastlane beta
@@ -145,7 +151,14 @@ jobs:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
# - name: Assemble WearOS production and upload to Google Play
# if: "!contains(steps.tagger.outputs.tag, '-beta')"
# run: bundle exec fastlane production_wearos
# env:
# STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
- name: Assemble production and upload to Google Play
if: "!contains(steps.tagger.outputs.tag, '-beta')"
@@ -153,5 +166,4 @@ jobs:
env:
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
+42 -42
View File
@@ -1,27 +1,27 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
CFPropertyList (3.0.6)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.610.0)
aws-sdk-core (3.131.3)
aws-partitions (1.784.0)
aws-sdk-core (3.177.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (1.70.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-s3 (1.128.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@@ -30,14 +30,14 @@ GEM
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.92.4)
faraday (1.10.0)
excon (0.100.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -65,8 +65,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.208.0)
fastimage (2.2.7)
fastlane (2.213.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -90,7 +90,7 @@ GEM
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
@@ -106,9 +106,9 @@ GEM
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.25.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.0)
google-apis-androidpublisher_v3 (0.45.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -117,27 +117,27 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.18.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.37.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.2.0)
googleauth (1.6.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -148,20 +148,20 @@ GEM
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.4.1)
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.11.0)
mini_magick (4.12.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.0.0)
multipart-post (2.3.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
plist (3.7.0)
public_suffix (5.0.1)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
@@ -178,7 +178,7 @@ GEM
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
@@ -194,7 +194,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
+28 -8
View File
@@ -20,23 +20,43 @@ On Android 12+ the newer and more fine grained `BLUETOOTH_SCAN` permission is us
Bluetooth Low Energy is a technology that devices like AirPods use to communicate their status to nearby devices.
CAPod requests location permissions because these permissions are required to work with Bluetooth Low Energy data.
This is a privacy measure on Android's side because you could determine someones location by scanning for Bluetooth devices:
If you know the physical location of a Bluetooth device (e.g. AirTags) you could use Bluetooth data to calculate your position.
This is a privacy measure on Android's side because you could determine someones location by scanning for Bluetooth
devices:
If you know the physical location of a Bluetooth device (e.g. AirTags) you could use Bluetooth data to calculate your
position.
### Location access in the background
CAPod uses the "location access in the background" permission (`ACCESS_BACKGROUND_LOCATION`) on Android 11 and older to receive Bluetooth Low Energy data while the app is in the background. This permission enables the "Show popup" and "Autoconnect" features and allows CAPod to react to nearby devices whil the app is closed.
CAPod uses the "location access in the background" permission (`ACCESS_BACKGROUND_LOCATION`) on Android 11 and older to
receive Bluetooth Low Energy data while the app is in the background. This permission enables the "Show popup" and "
Autoconnect" features and allows CAPod to react to nearby devices whil the app is closed.
## Automatic crash reports
## Automatic error reports
Anonymous device information may be collected in the event of an app crash or error.
*This was removed in v2.11.0+*
To do this the app uses the service "Bugsnag":
If an error occurs, an automated report may be sent to help me fix the issue.
This is optional and you can opt out of this in the settings.
Error reports are collected using "Bugsnag":
https://www.bugsnag.com/
Bugsnag's privacy policy can be found here:
https://docs.bugsnag.com/legal/privacy-policy/
Crash reports may contain device and app related information, e.g. your phone model, Android version and app version.
Error reports contain device and app information related to the error that occured.
Additional information about the error context may also be included, e.g. what this app did shortly before the error.
You can disable automatic reports in the app's settings.
Additional details about the type of data that is collected by the error tracking SDK can be found here:
https://docs.bugsnag.com/platforms/android/playstore-privacy/
Error reports are pseudonymous. Unless you tell me your install-ID, I don't know that an error report came from you.
Error reports are automatically deleted after 90 days.
## Debug logs
The app has a debug log feature that can be used to assist troubleshooting efforts. This feature creates a log file that contains verbose output of what the app is doing.
It is manually triggered by the user through an option in the app settings. The recorded log file can be shared through compatible apps (e.g. your email app) using the system's share dialog. As this log file may contain sensitive information (e.g. details about files or installed applications) it should only be shared with trusted parties.
+6 -1
View File
@@ -4,7 +4,8 @@
[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases)](https://github.com/d4rken-org/capod/releases/latest)
[![Code tests & eval](https://github.com/d4rken-org/capod/actions/workflows/code-checks.yml/badge.svg)](https://github.com/d4rken/capod/actions/workflows/code-checks.yml)
[![Crowdin](https://badges.crowdin.net/capod/localized.svg)](https://crowdin.com/project/capod)
[![Github All Releases](https://img.shields.io/github/downloads/d4rken-org/capod/total.svg)](https://github.com/d4rken-org/capod/releases/latest)
[![Github Downloads](https://img.shields.io/github/downloads/d4rken-org/capod/total.svg?label=GitHub%20Downloads&logo=github)](https://github.com/d4rken-org/capod/edit/main/README.md#download)
[![Google Play Downloads](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%24totalinstalls)](https://github.com/d4rken-org/capod/edit/main/README.md#download)
A companion app that adds support for AirPod specific features to Android:
@@ -27,9 +28,11 @@ Currently supported models:
* AirPods 3. Generation
* AirPods Pro 1. Generation
* AirPods Pro 2. Generation
* AirPods Pro 2. Generation (USB-C)
* AirPods Max
* Power Beats Pro
* Power Beats 3
* Power Beats 4
* Beats Solo 3
* Beats Studio 3
* Beats X
@@ -75,6 +78,8 @@ Currently supported models:
Travis & Rye, Erik & Sipes, Brandon & Teplov, Sam. (2019). Handoff All Your Privacy A Review of Apples Bluetooth
Low Energy Continuity Protocol. Proceedings on Privacy Enhancing Technologies. 2019. 34-53. 10.2478/popets-2019-0057.
* Celosia, Guillaume. (2020). Privacy Challenges in Wireless Communications of the Internet of Things.
* The [MagicPods](https://github.com/steam3d/MagicPods-Windows) project. If you are looking for "CAPod for Windows",
check it out.
## License
+1 -1
View File
@@ -1 +1 @@
2.10.0-rc0 21000000
2.14.0-rc0 21400000
-7
View File
@@ -22,10 +22,6 @@ android {
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
) ?: "bugsnag_apikey_placeholder"
}
buildFeatures {
@@ -100,7 +96,4 @@ dependencies {
addTesting()
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
}
+2 -4
View File
@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>
<application>
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application>
</manifest>
@@ -1,58 +0,0 @@
package eu.darken.capod.debug.autoreport
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BugsnagErrorHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val bugsnagLogger: BugsnagLogger,
private val debugSettings: DebugSettings,
) : OnErrorCallback {
override fun onError(event: Event): Boolean {
bugsnagLogger.injectLog(event)
TAB_APP.also { tab ->
event.addMetadata(tab, "gitSha", BuildConfigWrap.GIT_SHA)
event.addMetadata(tab, "buildTime", BuildConfigWrap.BUILDTIME)
context.tryFormattedSignature()?.let { event.addMetadata(tab, "signatures", it) }
}
return debugSettings.isAutoReportingEnabled.value && !eu.darken.capod.common.BuildConfigWrap.DEBUG
}
companion object {
private const val TAB_APP = "app"
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
fun Context.tryFormattedSignature(): String? = try {
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures?.let { sigs ->
val sb = StringBuilder("[")
for (i in sigs.indices) {
sb.append(sigs[i].hashCode())
if (i + 1 != sigs.size) sb.append(", ")
}
sb.append("]")
sb.toString()
}
} catch (e: Exception) {
log(WARN) { e.asLog() }
null
}
}
}
@@ -1,47 +0,0 @@
package eu.darken.capod.debug.autoreport
import com.bugsnag.android.Event
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.asLog
import java.lang.String.format
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BugsnagLogger @Inject constructor() : Logging.Logger {
// Adding one to the initial size accounts for the add before remove.
private val buffer: Deque<String> = ArrayDeque(BUFFER_SIZE + 1)
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
val line = "${System.currentTimeMillis()} ${priority.toLabel()}/$tag: $message"
synchronized(buffer) {
buffer.addLast(line)
if (buffer.size > BUFFER_SIZE) {
buffer.removeFirst()
}
}
}
fun injectLog(event: Event) {
synchronized(buffer) {
var i = 100
buffer.forEach { event.addMetadata("Log", format(Locale.ROOT, "%03d", i++), it) }
event.addMetadata("Log", format(Locale.ROOT, "%03d", i), event.originalError?.asLog())
}
}
companion object {
private const val BUFFER_SIZE = 200
private fun Logging.Priority.toLabel(): String = when (this) {
Logging.Priority.VERBOSE -> "V"
Logging.Priority.DEBUG -> "D"
Logging.Priority.INFO -> "I"
Logging.Priority.WARN -> "W"
Logging.Priority.ERROR -> "E"
Logging.Priority.ASSERT -> "WTF"
}
}
}
@@ -2,20 +2,15 @@ package eu.darken.capod.debug.autoreport
import android.app.Application
import android.content.Context
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.InstallId
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
import eu.darken.capod.common.debug.logging.Logging
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 javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
@Singleton
@@ -23,9 +18,6 @@ class GplayAutoReporting @Inject constructor(
@ApplicationContext private val context: Context,
private val debugSettings: DebugSettings,
private val installId: InstallId,
private val bugsnagLogger: Provider<BugsnagLogger>,
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
) : AutomaticBugReporter {
override fun setup(application: Application) {
@@ -34,35 +26,13 @@ class GplayAutoReporting @Inject constructor(
if (!isEnabled) return
ReLinker
.log { message -> log(TAG) { "ReLinker: $message" } }
.loadLibrary(application, "bugsnag-plugin-android-anr")
// Currently no 3rd party bug tracking
try {
val bugsnagConfig = Configuration.load(context).apply {
if (debugSettings.isAutoReportingEnabled.value) {
Logging.install(bugsnagLogger.get())
setUser(installId.id, null, null)
autoTrackSessions = true
addOnError(bugsnagErrorHandler.get())
addMetadata("App", "buildFlavor", BuildConfigWrap.FLAVOR)
log(TAG) { "Bugsnag setup done!" }
} else {
autoTrackSessions = false
addOnError(nopBugsnagErrorHandler.get())
log(TAG) { "Installing Bugsnag NOP error handler due to user opt-out!" }
}
}
Bugsnag.start(context, bugsnagConfig)
Bugs.reporter = this
} catch (e: IllegalStateException) {
log(TAG) { "Bugsnag API Key not configured." }
}
Bugs.reporter = this
}
override fun notify(throwable: Throwable) {
Bugsnag.notify(throwable)
log(TAG, WARN) { "notify($throwable)" }
}
companion object {
@@ -1,19 +0,0 @@
package eu.darken.capod.debug.autoreport
import com.bugsnag.android.Event
import com.bugsnag.android.OnErrorCallback
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NOPBugsnagErrorHandler @Inject constructor() : OnErrorCallback {
override fun onError(event: Event): Boolean {
log(WARN) { "Error, but skipping bugsnag due to user opt-out: ${event.originalError?.asLog()}" }
return false
}
}
@@ -0,0 +1,14 @@
package eu.darken.capod.common.preferences
import android.content.Context
import android.util.AttributeSet
import androidx.preference.SwitchPreferenceCompat
class MaterialSwitchPreference(context: Context, attrs: AttributeSet?) :
SwitchPreferenceCompat(context, attrs) {
init {
// Use material switch
widgetLayoutResource = eu.darken.capod.common.R.layout.preference_material_switch
}
}
@@ -8,6 +8,7 @@ import eu.darken.capod.common.error.ErrorEventSource
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.livedata.SingleLiveEvent
import eu.darken.capod.common.navigation.NavEventSource
import eu.darken.capod.common.navigation.navVia
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
@@ -31,4 +32,7 @@ abstract class ViewModel3(
.setupCommonEventHandlers(TAG) { "launchInViewModel()" }
.launchIn(vmScope)
fun NavDirections.navigate() {
navVia(navEvents)
}
}
@@ -40,6 +40,8 @@ class GeneralSettings @Inject constructor(
val isOffloadedBatchingDisabled = preferences.createFlowPreference("core.compat.offloaded.batching.disabled", false)
val useIndirectScanResultCallback = preferences.createFlowPreference("core.compat.indirectcallback.enabled", false)
val isOnboardingDone = preferences.createFlowPreference("core.onboarding.done", false)
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
monitorMode,
scannerMode,
@@ -10,7 +10,7 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import java.time.Duration
import java.time.Instant
import java.util.*
import java.util.UUID
import kotlin.math.abs
import kotlin.math.max
@@ -91,6 +91,10 @@ interface PodDevice {
"AirPods Pro 2",
R.drawable.devic_airpods_pro2_both
),
@Json(name = "airpods.pro2.usbc") AIRPODS_PRO2_USBC(
"AirPods Pro 2 USB-C",
R.drawable.devic_airpods_pro2_both
),
@Json(name = "airpods.max") AIRPODS_MAX(
"AirPods Max",
R.drawable.devic_headphones_generic
@@ -110,6 +114,9 @@ interface PodDevice {
@Json(name = "beats.powerbeats.3") POWERBEATS_3(
"Power Beats 3"
),
@Json(name = "beats.powerbeats.4") POWERBEATS_4(
"Power Beats 4"
),
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
"Power Beats Pro"
),
@@ -19,6 +19,7 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
@@ -26,6 +27,7 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeats4(factory: PowerBeats4.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
@@ -0,0 +1,89 @@
package eu.darken.capod.pods.core.apple.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
data class AirPodsPro2Usbc(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null
) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2_USBC
@get:DrawableRes
override val iconRes: Int
get() = R.drawable.devic_airpods_pro2_both
@get:DrawableRes
override val caseIcon: Int
get() = R.drawable.devic_airpods_pro2_case
@get:DrawableRes
override val leftPodIcon: Int
get() = R.drawable.devic_airpods_pro2_left
@get:DrawableRes
override val rightPodIcon: Int
get() = R.drawable.devic_airpods_pro2_right
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = AirPodsPro2Usbc(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.rssiSmoothed(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
private val DEVICE_CODE = 0x2420.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro2", "USBC")
}
}
@@ -0,0 +1,60 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
data class PowerBeats4(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_4
override val rssi: Int
get() = rssiAverage ?: super<SingleApplePods>.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = PowerBeats4(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
rssiAverage = result.rssiSmoothed(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x0D20.toUShort()
private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "4")
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false" />
@@ -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">"اسمح لـلتطبيق بإظهار الإشعارات المتعلقة بسماعات الأذن الخاصة بك، على سبيل المثال. حالتهم الحالية خلال الاتصال."</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">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">Mostra notificacions</string>
<string name="permission_post_notifications_description">"Permet que el CAPod 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">Zobrazit oznámení</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>
@@ -1,6 +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 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>
@@ -56,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">"Επιτρέψτε στην εφαρμογή να εμφανίζει ειδοποιήσεις σχετικά με τα AirPods σας, π.χ. την τρέχουσα κατάστασή τους ενώ είστε συνδεδεμένοι."</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 Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_grant_permission_action">Conceder permiso</string>
<string name="permission_bluetooth_connect_label">Conexión Bluetooth</string>
<string name="permission_bluetooth_connect_description">Esta aplicacion requiere el permiso \"Conexión Bluetooth\" para interactuar con los dispositivos vinculados e iniciar las conexiones.</string>
@@ -58,4 +58,6 @@
<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">Mostrar las notificaciones</string>
<string name="permission_post_notifications_description">"Permite que CAPod muestre las notificaciones sobre tus AirPods, por ejemplo, su estado actual mientras están conectados."</string>
</resources>
@@ -6,4 +6,6 @@
<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_label">Näita teateid</string>
<string name="permission_post_notifications_description">"Luba CAPodil näidata AirPodsi teateid, nt praegust olekut ühendatud olles."</string>
</resources>
@@ -19,12 +19,12 @@
<string name="permission_access_fine_location_label">Accéder à la position précise</string>
<string name="permission_access_fine_location_description">CAPod utilise lautorisation « position précise » pour recevoir les données « faible énergie » de Bluetooth. Votre casque-micro utilise la technologie « basse énergie » de Bluetooth pour diffuser son état. Cette appli nutilisera PAS les données Bluetooth pour déterminer votre position.</string>
<string name="permission_background_location_label">Accès à la position en arrière-plan</string>
<string name="permission_background_location_description">CAPod utilise « laccès à la position en arrière-plan » afin de permettre des fonctions telles qu’« Afficher une notification » et « Connexion automatique » alors que lappli est fermée. Laccès à la position en arrière-plan permet à lappli de recevoir les données « basse énergie » de Bluetooth. Elles ne seront PAS utilisées pour déterminer votre position géographie.</string>
<string name="permission_background_location_description">CAPod utilise « laccès à la position en arrière-plan » afin de permettre des fonctions telles qu’« Afficher une fenêtre surgissante » et « Connexion automatique » alors que lappli est fermée. Laccès à la position en arrière-plan permet à lappli de recevoir les données « basse énergie » de Bluetooth. Elles ne seront PAS utilisées pour déterminer votre position géographie.</string>
<string name="permission_ignore_battery_optimizations_label">Désactiver les optimisations de la pile/batterie</string>
<string name="permission_ignore_battery_optimizations_description">Les optimisations de la pile/batterie empêchent lappli de recevoir les données Bluetooth de manière fiable alors que lappli est en arrière-plan.</string>
<string name="permission_required_title">Lautorisation suivante est nécessaire :</string>
<string name="permission_system_alert_window_label">Fenêtre dalerte système</string>
<string name="permission_system_alert_window_description">Permettre à CAPod dapparaître par-dessus les autres applis pour rendre la fonction « Afficher une notification » possible.</string>
<string name="permission_system_alert_window_description">Permettre à CAPod dapparaître par-dessus les autres applis pour rendre la fonction « Afficher une fenêtre surgissante » possible.</string>
<string name="settings_scanner_mode_lowpower_label">Faible puissance</string>
<string name="settings_scanner_mode_balanced_label">Équilibré</string>
<string name="settings_scanner_mode_lowlatency_label">Faible latence</string>
@@ -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>
@@ -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">Tampilkan notifikasi</string>
<string name="permission_post_notifications_description">"Izinkan CAPod untuk menampilkan notifikasi tentang AirPods anda, contoh. status saat terhubung."</string>
</resources>
+11 -9
View File
@@ -21,26 +21,26 @@
<string name="permission_background_location_label">Accesso in background alla tua posizione</string>
<string name="permission_background_location_description">CAPod usa l\'accesso in background alla tua posizione per far funzionare opzioni come \"Mostra popup\" e \"AutoConnect\" mentre l\'applicazione é chiusa. L\'accesso in background abiliterà l\'applicazione nella ricezione di dati Bluetooth a basso consume mentre l\'app non é aperta. Questa applicazione non utilizza i dati Bluetooth per geolocalizzarti.</string>
<string name="permission_ignore_battery_optimizations_label">Disabilita ottimizzazione batteria</string>
<string name="permission_ignore_battery_optimizations_description">L\'ottimizzazione della batteria impedisce a questa applicazione di ricevere dati Bluetooth mentre l\'app é chiusa.</string>
<string name="permission_ignore_battery_optimizations_description">L\'ottimizzazione della batteria impedisce a questa applicazione di ricevere dati Bluetooth mentre l\'app è in esecuzione in background.</string>
<string name="permission_required_title">È richiesta la seguente autorizzazione:</string>
<string name="permission_system_alert_window_label">Finestra di avviso di sistema</string>
<string name="permission_system_alert_window_description">Permetti a CAPod di far apprarire overlay sopra le altre applicazioni per far funzionare \"Mostra PopUp\".</string>
<string name="permission_system_alert_window_description">Concedi a CAPod la visualizzazione in primo piano sopra le altre applicazioni per la funzione \"Mostra PopUp\".</string>
<string name="settings_scanner_mode_lowpower_label">Basso consumo</string>
<string name="settings_scanner_mode_balanced_label">Bilanciato</string>
<string name="settings_scanner_mode_lowlatency_label">Bassa latenza</string>
<string name="settings_monitor_mode_manual_label">Quando l\'applicazione è aperta</string>
<string name="settings_monitor_mode_automatic_label">Quando il device è connesso</string>
<string name="settings_monitor_mode_automatic_label">Quando le cuffie sono connesse</string>
<string name="settings_monitor_mode_always_label">Sempre</string>
<string name="settings_reaction_autoconnect_whenseen_label">Quando rilevato</string>
<string name="settings_reaction_autoconnect_caseopen_label">La custodia é aperta</string>
<string name="settings_reaction_autoconnect_inear_label">Nell\'orecchio</string>
<string name="pods_dual_left_label">Pod sinistra</string>
<string name="pods_dual_right_label">Pod destra</string>
<string name="settings_reaction_autoconnect_inear_label">Indossato</string>
<string name="pods_dual_left_label">Pod SX</string>
<string name="pods_dual_right_label">Pod DX</string>
<string name="pods_case_label">Custodia</string>
<string name="pods_case_status_open_label">Aperto</string>
<string name="pods_case_status_closed_label">Chiuso</string>
<string name="pods_connection_state_disconnected_label">Non connesso</string>
<string name="pods_connection_state_idle_label">Connesso ad un device, ma inattivo</string>
<string name="pods_connection_state_idle_label">Connesso al dispositivo, ma inattivo</string>
<string name="pods_connection_state_music_label">In modalità musica</string>
<string name="pods_connection_state_call_label">In modalità chiamata</string>
<string name="pods_connection_state_ringing_label">Squilla</string>
@@ -48,14 +48,16 @@
<string name="pods_connection_state_unknown_label">Stato di connessione sconosciuto</string>
<string name="pods_unknown_raw_data_label">Dati non elaborati</string>
<string name="pods_unknown_label">Dispositivo sconosciuto</string>
<string name="pods_unknown_contact_dev">Questo é un dispositivo sconosciutom ma sta utilizzando un formato di comunicazione noto. Aggiungiamo il supporto per questo device, Contattami! :)</string>
<string name="pods_unknown_contact_dev">Questo è un dispositivo sconosciuto, ma sta utilizzando un formato di comunicazione noto. Aggiungiamo il supporto per questo dispositivo, contattami :)</string>
<string name="pods_none_label_short">Nessun dispositivo</string>
<string name="pods_charging_label">In ricarica</string>
<string name="pods_inear_label">Nell\'orecchio</string>
<string name="pods_inear_label">Indossato</string>
<string name="pods_microphone_label">Microfono</string>
<string name="pods_yours">Tuoi</string>
<string name="headset_being_worn_label">Indossato</string>
<string name="pods_case_unknown_state">Stato sconosciuto</string>
<string name="last_seen_x">Ultimo accesso: %s</string>
<string name="first_seen_x">Primo accesso: %s</string>
<string name="permission_post_notifications_label">Mostra notifiche</string>
<string name="permission_post_notifications_description">"Concedi a CAPod di mostrare notifiche riguardanti le tue AirPods, ad esempio il loro stato di carica mentre sono connesse."</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 Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="general_value_not_available_label">T/A</string>
<string name="general_error_label">Ralat</string>
<string name="general_grant_permission_action">Berikan kebenaran</string>
@@ -55,4 +58,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">Tunjukkan pemberitahuanm</string>
<string name="permission_post_notifications_description">"Benarkan CAPod memaparkan pemberitahuan tentang AirPods anda, misalnya, status semasa mereka ketika disambungkan."</string>
</resources>
@@ -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">Toon meldingen</string>
<string name="permission_post_notifications_description">"Sta CAPod toe om meldingen over je AirPods weer te geven, bijv. hun huidige status terwijl ze verbonden zijn."</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">Wyświetl powiadomienia</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>
@@ -58,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">Mostrar 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>
@@ -58,4 +58,6 @@
<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>
+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">Eroare</string>
<string name="general_grant_permission_action">Acordați permisiuni</string>
<string name="overview_nomaindevice_label">Niciun dispozitiv principal</string>
<string name="overview_nomaindevice_description">Este puțin probabil ca toate dispozitivele detectate să fie ale tale. Porniți și conectați dispozitivul sau ajustați setările.</string>
<string name="overview_bluetooth_disabled_label">Bluetooth este dezactivat</string>
<string name="overview_bluetooth_disabled_description">Bluetooth este dezactivat, activați-l ;)</string>
<string name="permission_bluetooth_connect_label">Conectare Bluetooth</string>
<string name="permission_bluetooth_connect_description">Această aplicație necesită permisiunea „Conectare Bluetooth” pentru a interacționa cu dispozitivele asociate și a iniția conexiuni.</string>
<string name="permission_bluetooth_scan_label">Scanare Bluetooth</string>
<string name="permission_bluetooth_scan_description">Permisiunea „Scanare Bluetooth” permite acestei aplicații să descopere și să primească date Bluetooth de la dispozitivele din apropiere, cum ar fi AirPod-urile dvs.</string>
<string name="permission_bluetooth_label">Bluetooth</string>
<string name="permission_bluetooth_description">Această aplicație necesită permisiunea „Bluetooth” pentru a se conecta la dispozitive Bluetooth asociate.</string>
<string name="permission_access_fine_location_label">Permiteți locație exactă</string>
<string name="permission_access_fine_location_description">CAPod folosește permisiunea „locație exactă” pentru a primi date Bluetooth Energie Scăzută (BLE). Căștile dvs. folosesc tehnologia Bluetooth Energie Scăzută (BLE) pentru a-și transmite starea. Această aplicație NU va folosi date Bluetooth pentru a vă determina locația.</string>
<string name="permission_background_location_label">Permiteți acces la locație în fundal</string>
<string name="permission_background_location_description">CAPod folosește „accesul la locație în fundal” pentru a activa funcții precum „Afișare pop-up” și „Conectare Automată” în timp ce aplicația este închisă. Accesul la locație în fundal permite aplicației să primească date Bluetooth Energie Scăzută (BLE) în timp ce se află în fundal. Această aplicație NU va folosi date Bluetooth pentru a vă determina locația.</string>
<string name="permission_ignore_battery_optimizations_label">Dezactivați optimizări ale bateriei</string>
<string name="permission_ignore_battery_optimizations_description">Optimizările bateriei împiedică această aplicație să primească în mod fiabil date Bluetooth în timp ce aplicația este în fundal.</string>
<string name="permission_required_title">Este necesară următoarea permisiune:</string>
<string name="permission_system_alert_window_label">Fereastra de alertă de sistem</string>
<string name="permission_system_alert_window_description">Permiteți CAPod să apară peste alte aplicații pentru a face posibilă funcția „Afișați pop-up”.</string>
<string name="settings_scanner_mode_lowpower_label">Putere redusă</string>
<string name="settings_scanner_mode_balanced_label">Echilibrat</string>
<string name="settings_scanner_mode_lowlatency_label">Latență scăzută</string>
<string name="settings_monitor_mode_manual_label">Când aplicația este deschisă</string>
<string name="settings_monitor_mode_automatic_label">Când dispozitivul este conectat</string>
<string name="settings_monitor_mode_always_label">Mereu</string>
<string name="settings_reaction_autoconnect_whenseen_label">Când este detectat</string>
<string name="settings_reaction_autoconnect_caseopen_label">Carcasa este deschisă</string>
<string name="settings_reaction_autoconnect_inear_label">În ureche</string>
<string name="pods_dual_left_label">Casca stângă</string>
<string name="pods_dual_right_label">Casca dreaptă</string>
<string name="pods_case_label">Carcasă</string>
<string name="pods_case_status_open_label">Deschisă</string>
<string name="pods_case_status_closed_label">Închisă</string>
<string name="pods_connection_state_disconnected_label">Nu sunt conectate la un dispozitiv</string>
<string name="pods_connection_state_idle_label">Conectate la un dispozitiv, dar inactiv</string>
<string name="pods_connection_state_music_label">În modul muzică</string>
<string name="pods_connection_state_call_label">În modul apel</string>
<string name="pods_connection_state_ringing_label">Sună</string>
<string name="pods_connection_state_hanging_up_label">Închidere</string>
<string name="pods_connection_state_unknown_label">Starea conexiunii necunoscută</string>
<string name="pods_unknown_raw_data_label">Date neprelucrate</string>
<string name="pods_unknown_label">Dispozitiv necunoscut</string>
<string name="pods_unknown_contact_dev">Acesta este un dispozitiv necunoscut, dar folosește un format de mesaj similar. Să adăugăm suport pentru el, contactează-mă :)</string>
<string name="pods_none_label_short">Niciun dispozitiv</string>
<string name="pods_charging_label">Se încarcă</string>
<string name="pods_inear_label">În ureche</string>
<string name="pods_microphone_label">Microfon</string>
<string name="pods_yours">Ale tale</string>
<string name="headset_being_worn_label">Fiind purtate</string>
<string name="pods_case_unknown_state">Stare necunoscută</string>
<string name="last_seen_x">Vazute ultima dată: %s</string>
<string name="first_seen_x">Prima dată văzute: %s</string>
<string name="permission_post_notifications_label">Arată notificări</string>
<string name="permission_post_notifications_description">"Permiteți ca CAPod să afișeze notificări despre AirPod-urile dvs., de exemplu, starea lor actuală în timp ce sunt conectate."</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 zobrazoval upozornenia týkajúce sa vašich slúchadiel AirPods, napr. ich aktuálny stav počas pripojenia."</string>
</resources>
@@ -58,4 +58,6 @@
<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">Bildirimleri göster</string>
<string name="permission_post_notifications_description">"CAPod'un AirPods'larınız hakkında bildirimler göstermesine izin vermelisiniz, ör. cihaza bağlıyken mevcut durumları."</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>
@@ -53,4 +53,6 @@
<string name="pods_case_unknown_state">Trạng thái không xác định</string>
<string name="last_seen_x">Nhìn thấy lần cuối: %s</string>
<string name="first_seen_x">Nhìn thấy lần đầu: %s</string>
<string name="permission_post_notifications_label">Hiển thị thông báo</string>
<string name="permission_post_notifications_description">"Cho phép CAPod hiển thị thông báo về AirPods của bạn, ví dụ: tình trạng hiện tại của họ trong khi kết nối."</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 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>
@@ -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>
@@ -61,6 +61,7 @@
<string name="pods_microphone_label">Microphone</string>
<string name="pods_yours">Yours</string>
<string name="headset_being_worn_label">Being worn</string>
<string name="headset_not_being_worn_label">Not being worn</string>
<string name="pods_case_unknown_state">Unknown state</string>
<string name="last_seen_x">Last seen: %s</string>
@@ -3,80 +3,69 @@ package eu.darken.capod.common.flow
import eu.darken.capod.common.collections.mutate
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.instanceOf
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.*
import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.coroutine.runTest2
import testhelpers.flow.test
import java.io.IOException
import java.lang.Thread.sleep
import kotlin.concurrent.thread
import kotlin.coroutines.EmptyCoroutineContext
class DynamicStateFlowTest : BaseTest() {
// Without an init value, there isn't a way to keep using the flow
@Test
fun `exceptions on initialization are rethrown`() {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
val hotData = eu.darken.capod.common.flow.DynamicStateFlow<String>(
fun `exceptions on initialization are rethrown`() = runTest2(expectedError = IOException::class) {
val testScope = this
val hotData = DynamicStateFlow<String>(
loggingTag = "tag",
parentScope = testScope,
coroutineContext = Dispatchers.Unconfined,
startValueProvider = { throw IOException() }
)
runBlocking {
withTimeoutOrNull(500) {
// This blocking scope gets the init exception as the first caller
hotData.flow.firstOrNull()
} shouldBe null
}
testScope.advanceUntilIdle()
testScope.uncaughtExceptions.single() shouldBe instanceOf(IOException::class)
// This blocking scope gets the init exception as the first caller
hotData.flow.firstOrNull()
}
@Test
fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() = runTest2(autoCancel = true) {
val testScope = this
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
coEvery { valueProvider.invoke(any()) } returns "Test"
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
coroutineContext = Dispatchers.Unconfined,
startValueProvider = valueProvider,
)
testScope.apply {
runTest2(autoCancel = true) {
hotData.flow.first() shouldBe "Test"
hotData.flow.first() shouldBe "Test"
}
coVerify(exactly = 1) { valueProvider.invoke(any()) }
}
hotData.flow.first() shouldBe "Test"
hotData.flow.first() shouldBe "Test"
coVerify(exactly = 1) { valueProvider.invoke(any()) }
}
@Test
fun `value updates`() {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
fun `value updates`() = runTest2(autoCancel = true) {
val testScope = this
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
coEvery { valueProvider.invoke(any()) } returns 1
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
startValueProvider = valueProvider,
@@ -88,7 +77,6 @@ class DynamicStateFlowTest : BaseTest() {
(1..16).forEach { _ ->
thread {
(1..200).forEach { _ ->
sleep(10)
hotData.updateAsync(
onUpdate = { this + 1L },
onError = { throw it }
@@ -97,12 +85,16 @@ class DynamicStateFlowTest : BaseTest() {
}
}
runBlocking {
testCollector.await { list, _ -> list.size == 3201 }
testCollector.latestValues shouldBe (1..3201).toList()
}
advanceUntilIdle()
testCollector.await { list, _ -> list.size == 3201 }
testCollector.latestValues shouldBe (1..3201).toList()
advanceUntilIdle()
coVerify(exactly = 1) { valueProvider.invoke(any()) }
testCollector.cancelAndJoin()
}
data class TestData(
@@ -110,14 +102,13 @@ class DynamicStateFlowTest : BaseTest() {
)
@Test
fun `check multi threading value updates with more complex data`() {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
val valueProvider =
mockk<suspend CoroutineScope.() -> Map<String, eu.darken.capod.common.flow.DynamicStateFlowTest.TestData>>()
coEvery { valueProvider.invoke(any()) } returns mapOf("data" to eu.darken.capod.common.flow.DynamicStateFlowTest.TestData())
fun `check multi threading value updates with more complex data`() = runTest2(autoCancel = true) {
val testScope = this
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val valueProvider = mockk<suspend CoroutineScope.() -> Map<String, TestData>>()
coEvery { valueProvider.invoke(any()) } returns mapOf("data" to TestData())
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
startValueProvider = valueProvider,
@@ -140,20 +131,23 @@ class DynamicStateFlowTest : BaseTest() {
}
}
runBlocking {
testCollector.await { list, _ -> list.size == 4001 }
testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
}
advanceUntilIdle()
testCollector.await { list, _ -> list.size == 4001 }
testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
advanceUntilIdle()
coVerify(exactly = 1) { valueProvider.invoke(any()) }
testCollector.cancelAndJoin()
}
@Test
fun `only emit new values if they actually changed updates`() {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
fun `only emit new values if they actually changed updates`() = runTest2(autoCancel = true) {
val testScope = this
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
startValueProvider = { "1" },
@@ -167,26 +161,28 @@ class DynamicStateFlowTest : BaseTest() {
hotData.updateAsync { "2" }
hotData.updateAsync { "1" }
runBlocking {
testCollector.await { list, _ -> list.size == 3 }
testCollector.latestValues shouldBe listOf("1", "2", "1")
}
advanceUntilIdle()
testCollector.await { list, _ -> list.size == 3 }
testCollector.latestValues shouldBe listOf("1", "2", "1")
}
@Test
fun `multiple subscribers share the flow`() = runTest2(autoCancel = true) {
val testScope = this
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
coEvery { valueProvider.invoke(any()) } returns "Test"
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = this,
parentScope = testScope,
startValueProvider = valueProvider,
)
val sub1 = hotData.flow.test(tag = "sub1", scope = this)
val sub2 = hotData.flow.test(tag = "sub2", scope = this)
val sub3 = hotData.flow.test(tag = "sub3", scope = this)
val sub1 = hotData.flow.test(tag = "sub1", scope = testScope)
val sub2 = hotData.flow.test(tag = "sub2", scope = testScope)
val sub3 = hotData.flow.test(tag = "sub3", scope = testScope)
hotData.updateAsync { "A" }
hotData.updateAsync { "B" }
@@ -207,17 +203,19 @@ class DynamicStateFlowTest : BaseTest() {
@Test
fun `value is persisted between unsubscribes`() = runTest2(autoCancel = true) {
val testScope = this
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
coEvery { valueProvider.invoke(any()) } returns 1
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = this,
parentScope = testScope,
coroutineContext = this.coroutineContext,
startValueProvider = valueProvider,
)
val testCollector1 = hotData.flow.test(tag = "collector1", scope = this)
val testCollector1 = hotData.flow.test(tag = "collector1", scope = testScope)
testCollector1.silent = false
(1..10).forEach { _ ->
@@ -233,7 +231,7 @@ class DynamicStateFlowTest : BaseTest() {
testCollector1.cancelAndJoin()
val testCollector2 = hotData.flow.test(tag = "collector2", scope = this)
val testCollector2 = hotData.flow.test(tag = "collector2", scope = testScope)
testCollector2.silent = false
advanceUntilIdle()
@@ -246,10 +244,10 @@ class DynamicStateFlowTest : BaseTest() {
}
@Test
fun `blocking update is actually blocking`() = runBlocking {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
fun `blocking update is actually blocking`() = runTest2(autoCancel = true) {
val testScope = this
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
coroutineContext = testScope.coroutineContext,
@@ -270,6 +268,8 @@ class DynamicStateFlowTest : BaseTest() {
hotData.updateBlocking { this - 3 } shouldBe 0
advanceUntilIdle()
testCollector.await { _, i -> i == 3 }
testCollector.latestValues shouldBe listOf(2, 3, 0)
@@ -277,10 +277,10 @@ class DynamicStateFlowTest : BaseTest() {
}
@Test
fun `blocking update rethrows error`() = runBlocking {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
fun `blocking update rethrows error`() = runTest2(autoCancel = true) {
val testScope = this
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
coroutineContext = testScope.coroutineContext,
@@ -300,16 +300,17 @@ class DynamicStateFlowTest : BaseTest() {
hotData.flow.first() shouldBe 2
hotData.updateBlocking { 3 } shouldBe 3
hotData.flow.first() shouldBe 3
testScope.uncaughtExceptions.singleOrNull() shouldBe null
advanceUntilIdle()
hotData.flow.first() shouldBe 3
testCollector.cancelAndJoin()
}
@Test
fun `async updates error handler`() = runTest2(expectedError = IOException::class) {
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = this,
startValueProvider = { 1 },
@@ -321,14 +322,15 @@ class DynamicStateFlowTest : BaseTest() {
hotData.updateAsync { throw IOException("Surprise") }
advanceUntilIdle()
testCollector.cancelAndJoin()
}
@Test
fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runBlocking {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runTest2(autoCancel = true) {
val testScope = this
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
startValueProvider = { 1 },
@@ -346,31 +348,29 @@ class DynamicStateFlowTest : BaseTest() {
testScope.advanceUntilIdle()
thrownError!!.shouldBeInstanceOf<IOException>()
testScope.uncaughtExceptions.singleOrNull() shouldBe null
testCollector.cancelAndJoin()
}
@Test
fun `clean up function is called when parent scope is cancelled`() = runTest {
val testScope =
createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + EmptyCoroutineContext)
fun `clean up function is called when parent scope is cancelled`() {
var onReleaseValue: String? = null
val hotData = eu.darken.capod.common.flow.DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
coroutineContext = Dispatchers.Unconfined,
startValueProvider = { "Test" },
onRelease = {
onReleaseValue = it
}
)
runTest2(autoCancel = true) {
val testScope = this
hotData.flow.first() shouldBe "Test"
val hotData = DynamicStateFlow(
loggingTag = "tag",
parentScope = testScope,
coroutineContext = Dispatchers.Unconfined,
startValueProvider = { "Test" },
onRelease = {
onReleaseValue = it
}
)
testScope.cancel()
hotData.flow.first() shouldBe "Test"
}
onReleaseValue shouldBe "Test"
}
@@ -0,0 +1,72 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class AirPodsPro2UsbcTest : BaseAirPodsTest() {
/**
* https://github.com/d4rken-org/capod/issues/164
*/
@Test
fun `AirPods Pro 2 with USB-C - via #164`() = runTest {
create<AirPodsPro2Usbc>("07 19 01 24 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x2420.toUShort()
rawStatus shouldBe 0x0B.toUByte()
rawPodsBattery shouldBe 0x99.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0xF.toUShort()
rawCaseLidState shouldBe 0x11.toUByte()
rawDeviceColor shouldBe 0x0.toUByte()
rawSuffix shouldBe 0x04.toUByte()
isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true
isLeftPodInEar shouldBe true
isRightPodInEar shouldBe true
batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.9f
isCaseCharging shouldBe false
isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
model shouldBe PodDevice.Model.AIRPODS_PRO2_USBC
}
}
@Test
fun `AirPods Pro 2 with USB-C - via #164 - in case`() = runTest {
create<AirPodsPro2Usbc>("07 19 01 24 20 53 AA 98 32 00 05 49 0A B8 BF 8E 29 D8 70 12 0D A7 0C CE 77 56 00") {
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
isLeftPodInEar shouldBe true
isRightPodInEar shouldBe false
batteryLeftPodPercent shouldBe 1f
batteryRightPodPercent shouldBe 1f
isCaseCharging shouldBe false
isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.8f
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
model shouldBe PodDevice.Model.AIRPODS_PRO2_USBC
}
}
}
@@ -40,4 +40,33 @@ class BeatsFitProTest : BaseAirPodsTest() {
model shouldBe PodDevice.Model.BEATS_FIT_PRO
}
}
@Test
fun `extra rl test case`() = runTest {
create<BeatsFitPro>("07 19 01 12 20 04 FA 92 54 11 24 CE B1 DF 9D 8D F5 E3 37 60 B1 23 8B 90 3B 63 3F") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1220.toUShort()
rawStatus shouldBe 0x04.toUByte()
rawPodsBattery shouldBe 0xFA.toUByte()
rawFlags shouldBe 0x9.toUShort()
rawCaseBattery shouldBe 0x2.toUShort()
rawCaseLidState shouldBe 0x54.toUByte()
rawDeviceColor shouldBe 0x11.toUByte()
rawSuffix shouldBe 0x24.toUByte()
isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true
batteryLeftPodPercent shouldBe null
batteryRightPodPercent shouldBe 1.0f
isCaseCharging shouldBe false
isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.2f
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
model shouldBe PodDevice.Model.BEATS_FIT_PRO
}
}
}
@@ -0,0 +1,75 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class PowerBeats4Test : BaseAirPodsTest() {
@Test
fun `playing music`() = runTest {
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 05 07 C5 E1 9C BD 9A 05 0E AE 9E 56 53 2F F9 75 4A") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0D20.toUShort()
rawStatus shouldBe 0x0.toUByte()
rawPodsBattery shouldBe 0x02.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x05.toUByte()
batteryHeadsetPercent shouldBe 0.2f
model shouldBe PodDevice.Model.POWERBEATS_4
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
}
}
@Test
fun `extra test case`() = runTest {
create<PowerBeats4>("07 19 01 0D 20 00 02 80 02 00 04 80 90 60 77 32 C2 0C 75 7A 3D 7E D7 1C 0E B8 43") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0D20.toUShort()
rawStatus shouldBe 0x0.toUByte()
rawPodsBattery shouldBe 0x02.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x02.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x04.toUByte()
batteryHeadsetPercent shouldBe 0.2f
model shouldBe PodDevice.Model.POWERBEATS_4
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
}
}
@Test
fun `disconnected state`() = runTest {
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 00 25 DF 66 40 AF 44 7B 77 95 8F D1 92 50 26 11 74") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0D20.toUShort()
rawStatus shouldBe 0x0.toUByte()
rawPodsBattery shouldBe 0x02.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
batteryHeadsetPercent shouldBe 0.2f
model shouldBe PodDevice.Model.POWERBEATS_4
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
}
}
}
@@ -1,24 +0,0 @@
package testhelpers.coroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
@ExperimentalCoroutinesApi
class CoroutinesTestExtension(
private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : BeforeEachCallback, AfterEachCallback,
TestCoroutineScope by createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + dispatcher) {
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
-6
View File
@@ -45,12 +45,9 @@ android {
create("gplay") {
dimension = "version"
signingConfig = signingConfigs["releaseGplay"]
extra["useBugsnag"] = true
}
}
setupBugsnagPlugin()
buildTypes {
val customProguardRules = fileTree(File("../proguard")) {
include("*.pro")
@@ -141,7 +138,4 @@ dependencies {
implementation("androidx.wear:wear:1.2.0")
implementation("androidx.wear.tiles:tiles-material:1.1.0")
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
}
+3 -2
View File
@@ -4,7 +4,8 @@
package="eu.darken.capod">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.type.watch" />
<application
@@ -41,4 +42,4 @@
tools:node="remove" />
</application>
</manifest>
</manifest>
@@ -7,7 +7,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bluetooth_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
@@ -20,7 +20,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bluetooth_description"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@@ -18,7 +18,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
@@ -31,7 +31,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/body"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@@ -8,7 +8,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/request_description"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
@@ -20,7 +20,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
@@ -32,7 +32,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_description"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@@ -44,11 +44,12 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/grant_action"
style="@style/Widget.MaterialComponents.Button"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/general_grant_permission_action"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/permission_description" />
@@ -69,11 +69,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.2">
app:layout_constraintTop_toTopOf="@+id/pod_right_container">
<ImageView
android:id="@+id/pod_left_icon"
@@ -199,8 +197,7 @@
android:layout_marginEnd="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pod_case_container"
app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.2">
app:layout_constraintTop_toBottomOf="@id/barrier_top">
<ImageView
android:id="@+id/pod_right_icon"
+8 -9
View File
@@ -38,16 +38,18 @@ android {
create("foss") {
dimension = "version"
signingConfig = signingConfigs["releaseFoss"]
// The info block is encrypted and can only be read by google
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
}
create("gplay") {
dimension = "version"
signingConfig = signingConfigs["releaseGplay"]
extra["useBugsnag"] = true
}
}
setupBugsnagPlugin()
buildTypes {
val customProguardRules = fileTree(File(projectDir, "proguard")) {
include("*.pro")
@@ -128,7 +130,7 @@ android {
dependencies {
implementation(project(":app-common"))
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
addBaseKotlin()
@@ -148,9 +150,6 @@ dependencies {
addTesting()
"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")
"gplayImplementation"("com.android.billingclient:billing:6.0.1")
"gplayImplementation"("com.android.billingclient:billing-ktx:6.0.1")
}
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Donează</string>
<string name="foss_upgrade_alreadydonated_label">Am donat deja</string>
<string name="foss_upgrade_no_money_label">Mi-am cheltuit toți banii pe AirPods</string>
</resources>
@@ -1,7 +1,13 @@
package eu.darken.capod.common.upgrade.core.client
import android.app.Activity
import com.android.billingclient.api.*
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.QueryProductDetailsParams
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
@@ -20,11 +26,15 @@ data class BillingClientConnection(
) {
private val purchasesLocal = MutableStateFlow<Collection<Purchase>>(emptySet())
val purchases: Flow<Collection<Purchase>> = combine(purchasesGlobal, purchasesLocal) { global, local ->
val combined = mutableMapOf<String, Purchase>()
global.plus(local).toSet().sortedByDescending { it.purchaseTime }.forEach { purchase ->
combined[purchase.orderId] = purchase
}
combined.values
val combined = mutableSetOf<Purchase>()
combined.addAll(local)
global
.let { purchases -> purchases.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED } }
.let { combined.addAll(it) }
combined.sortedByDescending { it.purchaseTime }
}
.setupCommonEventHandlers(TAG) { "purchases" }
+4 -1
View File
@@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="upgrades_gplay_unavailable_error">Serviciile Google Play nu sunt disponibile.</string>
<string name="upgrades_no_purchases_found_check_account">Nu s-au găsit achiziții. Folosești contul corect?</string>
</resources>
+12 -3
View File
@@ -9,6 +9,9 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
@@ -24,9 +27,9 @@
<activity
android:name=".main.ui.MainActivity"
android:exported="true"
android:theme="@style/AppThemeSplash"
android:label="@string/app_name"
android:launchMode="singleTop">
android:launchMode="singleTop"
android:theme="@style/AppThemeSplash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -74,7 +77,13 @@
android:name=".common.debug.recording.ui.RecorderActivity"
android:theme="@style/AppThemeFloating" />
<service android:name=".common.debug.recording.core.RecorderService" />
<service
android:name=".common.debug.recording.core.RecorderService"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Users can record a debug log to help developers with bugs. The debug log recording is controlled via notifications and hosted in a foreground service." />
</service>
<!-- Worker stuff-->
<service
@@ -0,0 +1,42 @@
package eu.darken.capod.common.preferences
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import androidx.preference.Preference
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
class IntentPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
@StyleRes defStyleRes: Int = 0,
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
override fun setIntent(_intent: Intent?) {
super.setIntent(_intent)
_intent?.let {
intent
setOnPreferenceClickListener {
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
log(TAG, ERROR) { "Failed to launch $intent: $e" }
Toast.makeText(context, e.toString(), Toast.LENGTH_LONG).show()
}
true
}
}
}
companion object {
private val TAG = logTag("IntentPreference")
}
}
@@ -53,7 +53,7 @@ abstract class PreferenceFragment2
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
}
@@ -0,0 +1,29 @@
package eu.darken.capod.main.ui.onboarding
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.uix.Fragment3
import eu.darken.capod.common.viewbinding.viewBinding
import eu.darken.capod.databinding.OnboardingFragmentBinding
import javax.inject.Inject
@AndroidEntryPoint
class OnboardingFragment : Fragment3(R.layout.onboarding_fragment) {
override val vm: OnboardingFragmentVM by viewModels()
override val ui: OnboardingFragmentBinding by viewBinding()
@Inject lateinit var webpageTool: WebpageTool
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ui.goPrivacyPolicy.setOnClickListener { webpageTool.open(PrivacyPolicy.URL) }
ui.continueAction.setOnClickListener { vm.finishOnboarding() }
super.onViewCreated(view, savedInstanceState)
}
}
@@ -0,0 +1,27 @@
package eu.darken.capod.main.ui.onboarding
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.uix.ViewModel3
import eu.darken.capod.main.core.GeneralSettings
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@HiltViewModel
class OnboardingFragmentVM @Inject constructor(
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
private val dispatcherProvider: DispatcherProvider,
private val generalSettings: GeneralSettings,
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
fun finishOnboarding() = launch {
generalSettings.isOnboardingDone.value = true
OnboardingFragmentDirections.actionOnboardingFragmentToOverviewFragment().navigate()
}
companion object {
val TAG = logTag("Onboarding", "Fragment", "VM")
}
}
@@ -11,7 +11,6 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.flow.combine
import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.common.livedata.SingleLiveEvent
import eu.darken.capod.common.navigation.navVia
import eu.darken.capod.common.permissions.Permission
import eu.darken.capod.common.uix.ViewModel3
import eu.darken.capod.common.upgrade.UpgradeRepo
@@ -48,6 +47,12 @@ class OverviewFragmentVM @Inject constructor(
private val bluetoothManager: BluetoothManager2,
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
init {
if (!generalSettings.isOnboardingDone.value) {
OverviewFragmentDirections.actionOverviewFragmentToOnboardingFragment().navigate()
}
}
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
val launchUpgradeFlow = SingleLiveEvent<(Activity) -> Unit>()
@@ -124,7 +129,7 @@ class OverviewFragmentVM @Inject constructor(
items.add(0, BluetoothDisabledVH.Item)
} else if (mainPod == null) {
items.add(0, MissingMainDeviceVH.Item {
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navVia(this)
OverviewFragmentDirections.actionOverviewFragmentToTroubleShooterFragment().navigate()
})
}
}
@@ -140,12 +145,14 @@ class OverviewFragmentVM @Inject constructor(
showDebug = isDebugMode,
isMainPod = isMainPod,
)
is SinglePodDevice -> SinglePodsCardVH.Item(
now = now,
device = it,
showDebug = isDebugMode,
isMainPod = isMainPod,
)
else -> UnknownPodDeviceCardVH.Item(
now = now,
device = it,
@@ -166,7 +173,7 @@ class OverviewFragmentVM @Inject constructor(
}
fun goToSettings() = launch {
OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navVia(this@OverviewFragmentVM)
OverviewFragmentDirections.actionOverviewFragmentToSettingsFragment().navigate()
}
fun onUpgrade() = launch {
@@ -8,6 +8,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.coroutine.AppScope
@@ -41,6 +42,7 @@ import kotlinx.coroutines.withTimeout
import java.time.Duration
import javax.inject.Inject
@AndroidEntryPoint
class WidgetProvider : AppWidgetProvider() {
@@ -95,6 +97,27 @@ class WidgetProvider : AppWidgetProvider() {
}
}
/**
* Returns number of cells needed for given size of the widget.
*
* The value is determined in accordance with official guidelines
* for designing widgets, see:
* https://developer.android.com/guide/practices/ui_guidelines/widget_design
*
* Thanks to Jakub S. on Stackoverflow for this solution:
* https://stackoverflow.com/a/37522648/10866268
*
* @param size Widget size in dp.
* @return Size in number of cells.
*/
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
++n
}
return n - 1
}
private suspend fun updateWidget(
context: Context,
widgetManager: AppWidgetManager,
@@ -107,7 +130,23 @@ class WidgetProvider : AppWidgetProvider() {
val layout = when {
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context)
device is DualPodDevice -> createDualPodLayout(context, device)
device is DualPodDevice -> {
val minWidth = widgetManager.getAppWidgetOptions(widgetId)
.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
val columns = getCellsForSize(minWidth)
/* Enable wide widgets only when we are provided with 5 or
* more columns of space.
* Although the minimum size is only 2x1, the safeguards are
* added if these restrictions will ever be loosened in the future.
*/
val layout = when (columns) {
in 1 .. 4 -> R.layout.widget_pod_dual_compact_layout
else -> R.layout.widget_pod_dual_wide_layout
}
createDualPodLayout(context, device, layout)
}
device is SinglePodDevice -> createSinglePodLayout(context, device)
device is PodDevice -> createUnknownPodLayout(context, device)
else -> createNoDeviceLayout(context)
@@ -169,8 +208,9 @@ class WidgetProvider : AppWidgetProvider() {
private fun createDualPodLayout(
context: Context,
podDevice: DualPodDevice,
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_dual_layout).apply {
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
@LayoutRes layout: Int
): RemoteViews = RemoteViews(context.packageName, layout).apply {
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice), layout=${layout}" }
val pendingIntent: PendingIntent = PendingIntent.getActivity(
context,
0,
@@ -80,8 +80,6 @@ class MonitorWorker @AssistedInject constructor(
val start = System.currentTimeMillis()
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
setForeground(monitorNotifications.getForegroundInfo(null))
doDoWork()
val duration = System.currentTimeMillis() - start
@@ -108,6 +106,8 @@ class MonitorWorker @AssistedInject constructor(
return
}
setForeground(monitorNotifications.getForegroundInfo(null))
val monitorJob = podMonitor.mainDevice
.setupCommonEventHandlers(TAG) { "PodMonitor" }
.distinctUntilChanged()
@@ -19,7 +19,16 @@ import eu.darken.capod.common.debug.logging.logTag
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.DualPodDevice
import eu.darken.capod.pods.core.HasCase
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.SinglePodDevice
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 kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
@@ -71,9 +80,65 @@ class MonitorNotifications @Inject constructor(
}
return builder.apply {
// Options here should be mutually exclusive, and are prioritized by their order of importance
// Some options are omitted here, as they will conflict with other options
// TODO: Implement a settings pane to allow user to customize this
val stateText = when {
// Pods charging state
// This goes first as pods should not be worn if it is still charging
device is HasChargeDetection && device.isHeadsetBeingCharged -> {
context.getString(eu.darken.capod.common.R.string.pods_charging_label)
}
// Pods wear state
device is HasEarDetection -> {
if (device.isBeingWorn) context.getString(eu.darken.capod.common.R.string.headset_being_worn_label)
else context.getString(eu.darken.capod.common.R.string.headset_not_being_worn_label)
}
// Case charge state
// This is under pods wear state as we don't want it conflicting with it
device is HasCase && device.isCaseCharging -> {
context.getString(eu.darken.capod.common.R.string.pods_charging_label)
}
else -> context.getString(eu.darken.capod.common.R.string.pods_case_unknown_state)
}
val batteryText = when (device) {
is DualPodDevice -> {
val left = device.getBatteryLevelLeftPod(context)
val right = device.getBatteryLevelRightPod(context)
when {
device is HasCase -> {
val case = device.getBatteryLevelCase(context)
"$left $case $right"
}
else -> "$left $right"
}
}
is SinglePodDevice -> {
val headset = device.getBatteryLevelHeadset(context)
when {
device is HasCase -> {
val case = device.getBatteryLevelCase(context)
"$headset $case"
}
else -> headset
}
}
else -> "?"
}
setStyle(NotificationCompat.DecoratedCustomViewStyle())
setCustomContentView(notificationViewFactory.createContentView(device))
setCustomBigContentView(notificationViewFactory.createContentView(device))
setSmallIcon(device.iconRes)
setContentTitle("$batteryText ~ $stateText")
setSubText(null)
log(TAG, VERBOSE) { "updatingNotification(): $device" }
}
@@ -13,7 +13,7 @@
<TextView
android:id="@+id/label_path"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
@@ -30,7 +30,7 @@
<TextView
android:id="@+id/recording_path"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
@@ -44,7 +44,7 @@
<TextView
android:id="@+id/label_size"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
@@ -58,7 +58,7 @@
<TextView
android:id="@+id/recording_size"
style="@style/TextAppearance.AppCompat.Body1"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
@@ -70,7 +70,7 @@
<TextView
android:id="@+id/label_compressed_size"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
@@ -83,7 +83,7 @@
<TextView
android:id="@+id/recording_size_compressed"
style="@style/TextAppearance.AppCompat.Body1"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/label_compressed_size"
+1 -1
View File
@@ -7,7 +7,7 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
style="@style/Widget.Material3.Toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/mascot"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginTop="48dp"
android:src="@drawable/splash_graphic2"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
style="@style/TextAppearance.Material3.HeadlineLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/app_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/mascot" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/onboarding_body1"
app:layout_constraintTop_toBottomOf="@id/title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/body2"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/onboarding_body2"
app:layout_constraintTop_toBottomOf="@id/body1" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/body3"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/onboarding_body3"
app:layout_constraintTop_toBottomOf="@id/body2" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/body4"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/onboarding_body4"
app:layout_constraintTop_toBottomOf="@id/body3" />
<com.google.android.material.button.MaterialButton
android:id="@+id/go_privacy_policy"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="32dp"
android:text="@string/settings_privacy_policy_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/body4" />
<com.google.android.material.button.MaterialButton
android:id="@+id/continue_action"
style="@style/Widget.Material3.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="32dp"
android:text="@string/general_continue_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/go_privacy_policy" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
@@ -15,7 +15,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bluetooth_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -27,7 +27,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bluetooth_description"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -15,7 +15,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -27,7 +27,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_description"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -15,7 +15,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/request_description"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
@@ -27,7 +27,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -39,7 +39,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/permission_description"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_marginTop="4dp"
android:layout_marginHorizontal="16dp"
@@ -51,7 +51,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/privacy_policy"
style="@style/TextAppearance.MaterialComponents.Tooltip"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_height="wrap_content"
@@ -63,7 +63,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/grant_action"
style="@style/Widget.MaterialComponents.Button"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_height="wrap_content"
@@ -28,7 +28,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -44,7 +44,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/last_seen"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/first_seen"
@@ -56,7 +56,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/first_seen"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
@@ -68,7 +68,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/reception"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -82,7 +82,7 @@
<ImageView
android:id="@+id/reception_icon"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
@@ -432,7 +432,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/status"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -28,7 +28,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -44,7 +44,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/last_seen"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
@@ -56,7 +56,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/reception"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -70,7 +70,7 @@
<ImageView
android:id="@+id/reception_icon"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
@@ -175,7 +175,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/status"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -15,7 +15,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
@@ -30,7 +30,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/last_seen"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
@@ -41,7 +41,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/reception"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -55,7 +55,7 @@
<ImageView
android:id="@+id/reception_icon"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
@@ -71,7 +71,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/details"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -86,7 +86,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/rawdata_label"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -101,7 +101,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/rawdata"
style="@style/TextAppearance.MaterialComponents.Body2"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
@@ -17,7 +17,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
@@ -30,7 +30,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
@@ -72,7 +72,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_left_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -116,7 +116,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_case_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -159,7 +159,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_right_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -17,7 +17,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
@@ -30,7 +30,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal"
style="@style/TextAppearance.MaterialComponents.Caption"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
@@ -63,7 +63,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -8,7 +8,7 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
style="@style/Widget.Material3.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
@@ -7,7 +7,7 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
style="@style/Widget.Material3.Toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
@@ -106,6 +106,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="12sp"
android:gravity="center"
android:textStyle="bold"
tools:text="AirPods Max" />
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/PodWidget.Container"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/pod_left_icon"
style="@style/PodInfoItemIcon.Notification.Large"
android:src="@drawable/devic_airpods_pro2_left"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/pod_left_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="4dp"
tools:text="100%" />
<ImageView
android:id="@+id/pod_left_charging"
style="@style/PodInfoItemIcon.Notification"
android:src="@drawable/ic_baseline_power_24"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/pod_left_ear"
style="@style/PodInfoItemIcon.Notification"
android:src="@drawable/ic_baseline_hearing_24"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/pod_case_icon"
style="@style/PodInfoItemIcon.Notification.Large"
android:src="@drawable/devic_airpods_gen1_case"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/pod_case_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="4dp"
tools:text="100%" />
<ImageView
android:id="@+id/pod_case_charging"
style="@style/PodInfoItemIcon.Notification"
android:src="@drawable/ic_baseline_power_24" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/pod_right_icon"
style="@style/PodInfoItemIcon.Notification.Large"
android:src="@drawable/devic_airpods_pro2_right"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/pod_right_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="4dp"
tools:text="100%" />
<ImageView
android:id="@+id/pod_right_charging"
style="@style/PodInfoItemIcon.Notification"
android:src="@drawable/ic_baseline_power_24"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/pod_right_ear"
style="@style/PodInfoItemIcon.Notification"
android:src="@drawable/ic_baseline_hearing_24"
tools:ignore="ContentDescription" />
</LinearLayout>
</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:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:textSize="12sp"
android:textStyle="bold"
tools:text="AirPods Max" />
</LinearLayout>
@@ -4,15 +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:textStyle="bold"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="AirPods Max" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -45,4 +36,14 @@
android:src="@drawable/ic_baseline_hearing_24"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/headphones_label"
style="@style/PodWidget.TextPrimary"
android:layout_width="wrap_content"
android:textStyle="bold"
android:gravity="center"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="AirPods Max" />
</LinearLayout>
+15
View File
@@ -16,6 +16,11 @@
<action
android:id="@+id/action_overviewFragment_to_troubleShooterFragment"
app:destination="@id/troubleShooterFragment" />
<action
android:id="@+id/action_overviewFragment_to_onboardingFragment"
app:popUpTo="@id/overviewFragment"
app:popUpToInclusive="true"
app:destination="@id/onboardingFragment" />
</fragment>
<fragment
@@ -27,5 +32,15 @@
android:id="@+id/troubleShooterFragment"
android:name="eu.darken.capod.troubleshooter.ui.TroubleShooterFragment"
tools:layout="@layout/troubleshooter_fragment" />
<fragment
android:id="@+id/onboardingFragment"
android:name="eu.darken.capod.main.ui.onboarding.OnboardingFragment"
tools:layout="@layout/onboarding_fragment">
<action
android:id="@+id/action_onboardingFragment_to_overviewFragment"
app:destination="@id/overviewFragment"
app:popUpToInclusive="true"
app:popUpTo="@id/onboardingFragment" />
</fragment>
</navigation>
+9
View File
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">Мадэль вашай прылады. Гэта дапамагае праграме апазнаць вашу прыладу, калі яна не падключана да вашага тэлефона.</string>
<string name="settings_onepod_mode_label">Рэжым аднаго навушніка</string>
<string name="settings_onepod_mode_description">Нашэнне абодвух навушнікаў не абавязкова, нашэнне аднаго навушніка цалкам дастаткова для запуску рэакцый.</string>
<string name="settings_popup_caseopen_label">Паказваць апавяшчэнне пры адкрыцці футляра</string>
<string name="settings_popup_caseopen_description">Паказваць апавяшчэнне, калі футляр прылады адчынены (эксперыментальная функцыя).</string>
<string name="settings_popup_connected_label">Паказваць апавяшчэнне пры падключэнні</string>
<string name="settings_popup_connected_description">Паказваць апавяшчэнне пры першапачатковым падключэнні прылады.</string>
<string name="notification_channel_device_status_label">Статус прылады</string>
<string name="debug_debuglog_size_label">Памер</string>
<string name="debug_debuglog_size_compressed_label">Сціснуты памер</string>
@@ -94,7 +97,13 @@
<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>
<string name="onboarding_body1">Гэта праграма дадае ў Android падтрымку спецыяльных функцый AirPod.</string>
<string name="onboarding_body2">Не ўсе прылады Android цалкам падтрымліваюць дадатковыя функцыі AirPod. Ваша аперацыйная сістэма патрабуе карэктнай рэалізацыі функцыі Bluetooth Low Energy.</string>
<string name="onboarding_body3">CAPod не змяшчае рэкламу і не збірае вашы даныя.</string>
<string name="onboarding_body4">Вы можаце перайсці на CAPod Pro, каб атрымаць дадатковыя функцыі і такім чынам падтрымаць распрацоўку.</string>
<string name="general_continue_action">Працягнуць</string>
</resources>
+9
View File
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">El model del vostre dispositiu principal. Això ajuda l\'aplicació a reconèixer el vostre dispositiu quan no està connectat al vostre telèfon.</string>
<string name="settings_onepod_mode_label">Mode d\'un AirPod</string>
<string name="settings_onepod_mode_description">No cal portar els dos AirdPods, per provocar reaccions només cal portar-ne un.</string>
<string name="settings_popup_caseopen_label">Mostra la finestra emergent de la funda</string>
<string name="settings_popup_caseopen_description">Mostra una finestra emergent quan s\'obre la funda del dispositiu (experimental).</string>
<string name="settings_popup_connected_label">Mostra la finestra emergent de connexió</string>
<string name="settings_popup_connected_description">Mostra una finestra emergent quan el dispositiu es connecti per primera vegada.</string>
<string name="notification_channel_device_status_label">Estat del dispositiu</string>
<string name="debug_debuglog_size_label">Mida</string>
<string name="debug_debuglog_size_compressed_label">Mida comprimida</string>
@@ -94,7 +97,13 @@
<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 vostres 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 rebre dades indica un problema amb el sistema operatiu del 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>
<string name="onboarding_body1">Aquesta aplicació afegeix suport per a funcions específiques d\'AirPod a l\'Android.</string>
<string name="onboarding_body2">No tots els dispositius Android són compatibles completament amb les funcions addicionals d\'AirPod. El vostre sistema operatiu requereix una implementació de Bluetooth de baixa energia que funcioni correctament.</string>
<string name="onboarding_body3">El CAPod no conté anuncis i no recull les vostres dades.</string>
<string name="onboarding_body4">Podeu actualitzar a CAPod Pro per obtenir funcions addicionals i donar suport al desenvolupament.</string>
<string name="general_continue_action">Continua</string>
</resources>
+9
View File
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">Model vašeho hlavního zařízení. To pomůže aplikaci rozpoznat vaše zařízení, když není připojeno k telefonu.</string>
<string name="settings_onepod_mode_label">Režim jednoho sluchátka</string>
<string name="settings_onepod_mode_description">Není nutné nosit obě sluchátka, k vyvolání reakce stačí nosit jen jedno.</string>
<string name="settings_popup_caseopen_label">Zobrazit vyskakovací okno pouzdra</string>
<string name="settings_popup_caseopen_description">Zobrazit vyskakovací okno, pokud je pouzdro zařízení otevřeno (experimentální).</string>
<string name="settings_popup_connected_label">Zobrazit vyskakovací okno při připojení</string>
<string name="settings_popup_connected_description">Zobrazit vyskakovací okno při prvním připojení zařízení.</string>
<string name="notification_channel_device_status_label">Stav zařízení</string>
<string name="debug_debuglog_size_label">Velikost</string>
<string name="debug_debuglog_size_compressed_label">Komprimovaná velikost</string>
@@ -94,7 +97,13 @@
<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>
<string name="onboarding_body1">Tato aplikace přidává do Androidu podporu specifických funkcí AirPods.</string>
<string name="onboarding_body2">Ne všechna zařízení s Androidem plně podporují speciální funkce AirPods.</string>
<string name="onboarding_body3">CAPod neobsahuje žádné reklamy a neshromažďuje vaše údaje.</string>
<string name="onboarding_body4">Chcete-li získat speciální funkce a podpořit vývoj, můžete přejít na verzi CAPod Pro.</string>
<string name="general_continue_action">Pokračovat</string>
</resources>
+5
View File
@@ -101,4 +101,9 @@
<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>
<string name="onboarding_body1">Diese App fügt Support für AirPod-Spezifische funkionen zu Android hinzu.</string>
<string name="onboarding_body2">Nicht alle Android-Geräte unterstützen die zusätzlichen AirPod-Funktionen vollständig. Ihr Betriebssystem erfordert eine korrekt funktionierende Bluetooth-Low-Energy-Implementierung.</string>
<string name="onboarding_body3">CAPod hat keine Werbung und sammelt Ihre Daten nicht.</string>
<string name="onboarding_body4">Sie können auf CAPod Pro upgraden, um zusätzliche Funktionen zu erhalten und die Entwicklung zu unterstützen.</string>
<string name="general_continue_action">Fortfahren</string>
</resources>
+33 -1
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>
@@ -39,7 +45,10 @@
<string name="settings_maindevice_model_description">Το μοντέλο της κύριας συσκευής σας. Αυτό βοηθά την εφαρμογή να αναγνωρίσει τη συσκευή σας όταν δεν είναι συνδεδεμένη στο τηλέφωνό σας.</string>
<string name="settings_onepod_mode_label">Λειτουργία ενός pod</string>
<string name="settings_onepod_mode_description">Δεν απαιτείται να φοράτε και τα δύο pods, αρκεί να φοράτε ένα μόνο pod για να ενεργοποιούνται αντιδράσεις.</string>
<string name="settings_popup_caseopen_label">Εμφάνιση πληροφοριών θήκης</string>
<string name="settings_popup_caseopen_description">Εμφάνιση αναδυόμενου όταν η θήκη της συσκευής είναι ανοιχτή (πειραματικό).</string>
<string name="settings_popup_connected_label">Εμφάνιση παραθυράκι σύνδεσης</string>
<string name="settings_popup_connected_description">Εμφάνιση παραθύρου όταν η συσκευή συνδέεται για πρώτη φορά.</string>
<string name="notification_channel_device_status_label">Κατάσταση συσκευής</string>
<string name="debug_debuglog_size_label">Μέγεθος</string>
<string name="debug_debuglog_size_compressed_label">Συμπιεσμένο μέγεθος</string>
@@ -52,6 +61,7 @@
<string name="settings_support_description">Εάν χρειάζεστε κάποια βοήθεια.</string>
<string name="issue_tracker_label">Εργαλείο παρακολούθησης θεμάτων</string>
<string name="issue_tracker_description">Δημόσιο εργαλείο παρακολούθησης θεμάτων για αναφορές σφαλμάτων και αιτήματα για δυνατότητες (μόνο αγγλικά).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Ένα μέρος για να αράξετε και να κάνετε ερωτήσεις.</string>
<string name="changelog_label">Αρχείο αλλαγών</string>
<string name="settings_label">Ρυθμίσεις</string>
@@ -73,5 +83,27 @@
<string name="help_translate_label">Μετάφραση</string>
<string name="help_translate_description">Βοηθήστε να μεταφραστεί αυτή η εφαρμογή στην αγαπημένη σας γλώσσα.</string>
<string name="translators_thanks_title">Μεταφραστές</string>
<string name="translators_thanks_description">Vagelis1608</string>
<string name="translators_thanks_description">Vagelis1608;Theodoros Asimakopoulos;gogoboss_stampolidis;Matthias;Christos Chronopoulos</string>
<string name="widget_description">Ένα γραφικό στοιχείο που δείχνει την τελευταία γνωστή κατάσταση συσκευής.</string>
<string name="settings_compat_indirectcallback_title">Έμμεση παράδοση δεδομένων</string>
<string name="settings_compat_indirectcallback_summary">Χρησιμοποιήστε εναλλακτική μέθοδο για να λάβετε δεδομένα BLE από το σύστημα (broadcast αντί για callback).</string>
<string name="troubleshooter_title">Αντιμετώπιση προβλημάτων</string>
<string name="troubleshooter_ble_intro_title">Εκπομπές Χαμηλής Ενέργειας Bluetooth</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>
<string name="onboarding_body1">Αυτή η εφαρμογή προσθέτει υποστήριξη για συγκεκριμένες λειτουργίες του AirPod στο Android.</string>
<string name="onboarding_body2">Δεν υποστηρίζουν πλήρως όλες οι συσκευές Android τις επιπλέον δυνατότητες του AirPod. Το λειτουργικό σας σύστημα απαιτεί μια σωστή εφαρμογή Bluetooth-χαμηλής ενέργειας.</string>
<string name="onboarding_body3">Το CAPod δεν έχει διαφημίσεις και δε συλλέγει τα δεδομένα σας.</string>
<string name="onboarding_body4">Μπορείτε να αναβαθμίσετε στο CAPod Pro για να αποκτήσετε επιπλέον λειτουργίες και να υποστηρίξετε την ανάπτυξη.</string>
<string name="general_continue_action">Συνέχεια</string>
</resources>
+7 -1
View File
@@ -1,2 +1,8 @@
<?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="onboarding_body1">Esta aplicación agrega soporte para funciones específicas de los AirPods para Android.</string>
<string name="onboarding_body2">No todos los dispositivos Android son totalmente compatibles con las funciones adicionales de los AirPods. Tu sistema operativo requiere una implementación de un Bluetooth de baja energía que funcione correctamente.</string>
<string name="onboarding_body3">CAPod no contiene anuncios y no recopila tus datos.</string>
<string name="onboarding_body4">Puedes actualizar a CAPod Pro para obtener funciones adicionales y apoyar el desarrollo.</string>
<string name="general_continue_action">Sigue</string>
</resources>
+10 -1
View File
@@ -8,7 +8,7 @@
<string name="general_check_action">Verificar</string>
<string name="general_close_action">Cerrar</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="upgrade_capod_description">Consigue las funciones 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>
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">El modelo de su dispositivo principal. Esto ayuda a que la aplicación reconozca su dispositivo cuando no está conectado a su teléfono.</string>
<string name="settings_onepod_mode_label">Modo un audífono</string>
<string name="settings_onepod_mode_description">No es necesario usar ambos AirPods, usar un solo AirPod es suficiente para desencadenar reacciones.</string>
<string name="settings_popup_caseopen_label">Mostrar una ventana emergente</string>
<string name="settings_popup_caseopen_description">Muestra una ventana emergente cuando se abre el estuche del dispositivo (experimental).</string>
<string name="settings_popup_connected_label">Mostrar una ventana emergente con la conexión</string>
<string name="settings_popup_connected_description">Mostrar una ventana emergente cuando el dispositivo se conecta por primera vez.</string>
<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>
@@ -94,7 +97,13 @@
<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 intentar esta prueba en una zona concurrida para ver si se pueden recibir fuentes de datos (distintas de los auriculares). El hecho de que no se reciban 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>
<string name="onboarding_body1">Esta aplicación agrega soporte para funciones específicas de los AirPods para Android.</string>
<string name="onboarding_body2">No todos los dispositivos Android son totalmente compatibles con las funciones adicionales de los AirPods. Tu sistema operativo requiere una implementación en el Bluetooth de bajo consumo que funcione correctamente.</string>
<string name="onboarding_body3">CAPod no contiene anuncios y no recopila tus datos.</string>
<string name="onboarding_body4">Puedes actualizar a CAPod Pro para obtener funciones adicionales y apoyar el desarrollo.</string>
<string name="general_continue_action">Seguir</string>
</resources>
@@ -6,6 +6,9 @@
<string name="settings_compat_offloaded_filtering_disabled_summary">Süsteemis andmete filtreerimise asemel saate seda teha rakenduses.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Keela riistvara rühmitamine</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Keelake süsteemil rühmitada kogutud andmeid madalama energiatarbimisega Bluetoothi kohta enne meile saatmist.</string>
<string name="settings_popup_caseopen_label">Näita kaane hüpikakent</string>
<string name="settings_popup_connected_label">Näita ühenduse hüpikakent</string>
<string name="settings_popup_connected_description">Näita seadme esmaühendumisel hüpikakent.</string>
<string name="widget_description">Viimast teadaolevat seadme seisu näitab vidin.</string>
<string name="settings_compat_indirectcallback_title">Kaudsete andmete edastamine</string>
<string name="settings_compat_indirectcallback_summary">Süsteemis vähem energiat tarbiva Bluetoothi andmete saamine muul viisil (edasta tagasi kutsumise asemel).</string>
@@ -19,7 +22,13 @@
<string name="troubleshooter_ble_result_success_body">CAPod võtab vastu madalama energiakuluga Bluetoothi kaudu reklaame.</string>
<string name="troubleshooter_ble_result_failure_title">Nurjus</string>
<string name="troubleshooter_ble_result_failure_body">Parandamine nurjus. Ükski ühilduvuskombinatsioonidest ei toiminud.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Telefon ei võtnud vastu mingeid andmeid madalama energiakuluga Buetoothi kaudu. Üritage korrate rahvarohkes kohas, et näha, kas andmete allikaid (mis pole teie kõrvaklapid) saab kätte. Andmete saamise nurjumine viitab telefoni operatsioonisüsteemi veale.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Telefoni saabusid andmed madalama energiakuluga Bluetoothi kaudu aga andmed ei pärine ühestki ühilduvast seadmest. Kas CAPod ühildub teie kõrvaklapiga?</string>
<string name="troubleshooter_ble_result_failure_action">Proovi uuesti</string>
<string name="troubleshoot_action">Lahendamine</string>
<string name="onboarding_body1">See rakendus lisab Androidile AirPodi võimalused.</string>
<string name="onboarding_body2">Vaid mõned Androidi seadmed toetavad kõiki AirPodi võimalusi. Operatsioonisüsteem vajab õigesti toimivat madalama energiatarbimisega Bluetoothi tuge.</string>
<string name="onboarding_body3">CAPodis pole reklaame ja see ei kogu andmeid.</string>
<string name="onboarding_body4">Lisavõimaluste saamiseks ja arendamise toetamiseks soetage CAPod Pro.</string>
<string name="general_continue_action">Jätka</string>
</resources>
+3 -1
View File
@@ -1,2 +1,4 @@
<?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="general_share_action">Share</string>
</resources>
+12 -3
View File
@@ -4,7 +4,7 @@
<string name="general_done_action">Terminé</string>
<string name="general_copy_action">Copier</string>
<string name="general_thank_you_label">Merci</string>
<string name="general_upgrade_action">Surclasser vers Pro</string>
<string name="general_upgrade_action">Passer Pro</string>
<string name="general_check_action">Vérifier</string>
<string name="general_close_action">Fermer</string>
<string name="upgrade_capod_label">Surclasser CAPod</string>
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">Le modèle de votre appareil principal. Facilite la reconnaissance de votre appareil par lappli sil nest pas connecté à votre téléphone.</string>
<string name="settings_onepod_mode_label">Mode à un seul AirPod</string>
<string name="settings_onepod_mode_description">Il nest pas nécessaire de porter deux AirPods, en porter un seul suffit à déclencher les réactions.</string>
<string name="settings_popup_caseopen_description">Afficher une notification à louverture de l’étui de l\'appareil (expérimental).</string>
<string name="settings_popup_caseopen_label">Afficher une fenêtre pour l’étui</string>
<string name="settings_popup_caseopen_description">Afficher une fenêtre surgissante à louverture de l’étui de l\'appareil (expérimental).</string>
<string name="settings_popup_connected_label">Afficher une fenêtre de connection</string>
<string name="settings_popup_connected_description">Afficher une fenêtre surgissante quand lappareil se connecte pour la première fois.</string>
<string name="notification_channel_device_status_label">État de lappareil</string>
<string name="debug_debuglog_size_label">Taille</string>
<string name="debug_debuglog_size_compressed_label">Taille compressée</string>
@@ -80,7 +83,7 @@
<string name="help_translate_label">Traduction</string>
<string name="help_translate_description">Aider à traduire cette appli dans votre langue préférée.</string>
<string name="translators_thanks_title">Traducteurs</string>
<string name="translators_thanks_description">AO</string>
<string name="translators_thanks_description">yahoe-001</string>
<string name="widget_description">Un widget qui affiche le dernier état connu de lappareil.</string>
<string name="settings_compat_indirectcallback_title">Transmission indirecte des données</string>
<string name="settings_compat_indirectcallback_summary">Utiliser une méthode de remplacement pour recevoir les données BÉB du système (diffusion au lieu de rappel).</string>
@@ -94,7 +97,13 @@
<string name="troubleshooter_ble_result_success_body">Les diffusions dannonces BÉB sont reçues par CAPod.</string>
<string name="troubleshooter_ble_result_failure_title">Échec du dépannage</string>
<string name="troubleshooter_ble_result_failure_body">Le dépannage a échoué. Aucune des options de compatibilité na réglé la situation.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Votre téléphone na reçu aucune donnée BÉB. Vous pouvez relancer le test dans un endroit bondé pour voir si des sources de données (autres que votre casque d’écoute) sont reçues. Si aucune donnée nest reçue, le problème pourrait provenir du système dexploitation de votre téléphone.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Votre téléphone a reçu des données, mais les données ne proviennent pas dun appareil pris en charge. Votre casque d’écoute est-il en fonction? CAPod prend-il votre casque d’écoute en charge?</string>
<string name="troubleshooter_ble_result_failure_action">Réessayer</string>
<string name="troubleshoot_action">Dépanner</string>
<string name="onboarding_body1">Cette appli ajoute des fonctions propres aux AirPods à Android.</string>
<string name="onboarding_body2">Tous les appareils Android ne prennent pas en charge les fonctions supplémentaires des AirPods. Votre système dexploitation a besoin dune mise en œuvre fonctionnelle du système à basse énergie Bluetooth.</string>
<string name="onboarding_body3">CAPod ne présente aucune publicité et ne recueille pas vos données.</string>
<string name="onboarding_body4">Vous pouvez passer à CAPod Pro pour obtenir des fonctions supplémentaires et soutenir le développement.</string>
<string name="general_continue_action">Poursuivre</string>
</resources>
+9
View File
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">Model perangkat utama anda. Ini membantu aplikasi mengenali perangkat anda saat tidak terhubung ke ponsel anda.</string>
<string name="settings_onepod_mode_label">Mode satu pod</string>
<string name="settings_onepod_mode_description">Menggunakan kedua pod tidak diperlukan, memakai satu pod sudah cukup untuk memicu reaksi.</string>
<string name="settings_popup_caseopen_label">Tampilkan popup kasing</string>
<string name="settings_popup_caseopen_description">Tampilkan popup saat kasing perangkat dibuka (eksperimental).</string>
<string name="settings_popup_connected_label">Tampilkan popup saat terkoneksi</string>
<string name="settings_popup_connected_description">Tampilkan popup ketika perangkat terhubung pertama kalinya.</string>
<string name="notification_channel_device_status_label">Status perangkat</string>
<string name="debug_debuglog_size_label">Ukuran</string>
<string name="debug_debuglog_size_compressed_label">Ukuran terkompresi</string>
@@ -94,7 +97,13 @@
<string name="troubleshooter_ble_result_success_body">Siaran iklan BLE diterima oleh CAPod.</string>
<string name="troubleshooter_ble_result_failure_title">Gagal</string>
<string name="troubleshooter_ble_result_failure_body">Pemecahan masalah gagal. Tidak ada kombinasi opsi kompatibilitas yang membantu.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Ponsel Anda tidak menerima data BLE sama sekali. Anda dapat mencoba lagi pengujian ini di area yang ramai untuk melihat apakah sumber data (selain headphone Anda) dapat diterima. Tidak ada data yang diterima menunjukkan masalah dengan sistem operasi ponsel Anda.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Telepon anda menerima data BLE, tetapi data tersebut tidak berasal dari perangkat yang didukung. Apakah headphone Anda dihidupkan? Apakah CAPod mendukung headphone Anda?</string>
<string name="troubleshooter_ble_result_failure_action">Coba lagi</string>
<string name="troubleshoot_action">Memecahkan masalah</string>
<string name="onboarding_body1">Aplikasi ini menambahkan bantuan untuk fitur spesifik AirPod untuk Android.</string>
<string name="onboarding_body2">Tidak semua perangkat Android mendukung fitur AirPod ekstra. Sistem Os anda memerlukan implementasi Bluetooth-Low-Energy yang bekerja.</string>
<string name="onboarding_body3">CAPod tidak menampilkan iklan dan tidak mengambil data anda.</string>
<string name="onboarding_body4">Anda bisa membeli CAPod Pro agar bisa menggunakan fitur ekstra dan mendukung pengembangan aplikasi.</string>
<string name="general_continue_action">Lanjutkan</string>
</resources>
+40 -18
View File
@@ -7,34 +7,37 @@
<string name="general_upgrade_action">Aggiorna</string>
<string name="general_check_action">Controlla</string>
<string name="general_close_action">Chiudi</string>
<string name="upgrade_capod_label">Aggiorna CAPod</string>
<string name="upgrade_capod_label">Passa a CAPod PRO</string>
<string name="upgrade_capod_description">Ottieni funzionalità aggiuntive e supporta lo sviluppatore.</string>
<string name="settings_monitor_mode_label">Modalità monitor</string>
<string name="settings_monitor_mode_description">Sotto quali circostanze questa applicazione monitora i dati Bluetooth.</string>
<string name="settings_monitor_mode_label">Modalità di monitoraggio</string>
<string name="settings_monitor_mode_description">Circostanze in cui questa applicazione monitora i dati Bluetooth.</string>
<string name="settings_scanner_mode_label">Modalità di scansione</string>
<string name="settings_scanner_mode_description">Il bluetooth a basso consumo energetico dovrebbe dare priorità alle performance o al risparmio della batteria?</string>
<string name="settings_scanner_mode_description">La scansione dei dati BLE deve dare priorità alle performance o al risparmio di batteria?</string>
<string name="settings_autopause_label">Pausa automatica</string>
<string name="settings_autopause_description">Metti in pausa l\'audio quando viene rimossa la cuffia da un orecchio.</string>
<string name="settings_showall_label">Mostra tutti i dispositivi</string>
<string name="settings_showall_description">Mostra i dispositivi delle altre persone vicine a te.</string>
<string name="settings_autopplay_label">Riproduzione automatica</string>
<string name="settings_autoplay_description">Avvia riproduzione audio quando il dispositivo è indossato.</string>
<string name="settings_fake_data_label">Dati immaginari</string>
<string name="settings_fake_data_label">Dati fittizzi</string>
<string name="settings_fake_data_description">Mostra dati fittizi, ovvero simula dispositivi che in realtà non esistono.</string>
<string name="settings_debug_label">Impostazioni di debug</string>
<string name="settings_debug_description">Impostazioni aggiuntive che possono aiutare a risolvere i problemi con l\'applicazione.</string>
<string name="settings_signal_minimum_label">Qualità del segnale minima</string>
<string name="settings_signal_minimum_description">La minima qualità del segnale che il dispositivo necessità per poter essere considerato tuo.</string>
<string name="settings_debug_description">Impostazioni aggiuntive che possono aiutare a risolvere i problemi dell\'app.</string>
<string name="settings_signal_minimum_label">Qualità minima di segnale</string>
<string name="settings_signal_minimum_description">La qualità minima di segnale che il dispositivo necessita per essere considerato tuo.</string>
<string name="settings_autoconnect_label">Connessione automatica</string>
<string name="settings_autoconnect_description">Se android non si connette automaticamente, noi possiamo chiedergli di farlo. Questo imposterà la modalità di monitoramento su \'sempre\'.</string>
<string name="settings_autoconnect_description">Se Android non si connette automaticamente, possiamo chiedergli di farlo. Questo imposterà la modalità di monitoraggio su \'Sempre\'.</string>
<string name="settings_autoconnect_condition_label">Condizione per la connessione automatica</string>
<string name="settings_autoconnect_condition_description">Quando dovremmo provare a connetere il tuo dispositivo?</string>
<string name="settings_autoconnect_condition_description">Quando dovremmo provare a connettere il tuo dispositivo?</string>
<string name="settings_reaction_label">Reazioni</string>
<string name="settings_reaction_description">Reazione ad eventi o comportamenti.</string>
<string name="settings_reaction_description">Reazione a eventi o comportamenti.</string>
<string name="settings_category_yourdevice_label">Il tuo dispositivo</string>
<string name="settings_category_compatibility_options_title">Opzioni di compatibilità</string>
<string name="settings_category_compatibility_options_description">Non toccare se funziona tutto ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Disabilita il filtro hardware</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">Non delegare il filtraggio dei dati al sistema, invece ricevi tutti i dati e filtrali nell\'applicazione.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Disabilita il batching hardware</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Non concedere al sistema di raggruppare i dati BLE raccolti prima di inviarli a noi.</string>
<string name="settings_maindevice_address_label">L\'indirizzo del tuo dispositivo</string>
<string name="settings_maindevice_address_description">L\'indirizzo del dispositivo connesso. L\'applicazione usa questo per determinare quando il tuo dispositivo è connesso al telefono.</string>
<string name="settings_maindevice_address_none">Niente</string>
@@ -42,19 +45,22 @@
<string name="settings_maindevice_model_description">Il modello del tuo dispositivo principale. Questo aiuta la applicazione a riconoscere il tuo dispositivo quando non è connesso al tuo telefono.</string>
<string name="settings_onepod_mode_label">Modalità singolo pod</string>
<string name="settings_onepod_mode_description">Indossare entrambi i pod non è necessario, indossare un singolo pod è sufficiente per far funzionare le reazioni.</string>
<string name="settings_popup_caseopen_label">Mostra popup della custodia</string>
<string name="settings_popup_caseopen_description">Mostra un popup quando la custodia del dispositivo è aperta (sperimentale).</string>
<string name="settings_popup_connected_label">Mostra popup di connessione</string>
<string name="settings_popup_connected_description">Mostra un popup quando il dispositivo viene connesso per la prima volta.</string>
<string name="notification_channel_device_status_label">Stato dispositivo</string>
<string name="debug_debuglog_size_label">Dimensione</string>
<string name="debug_debuglog_size_compressed_label">Dimensione compressa</string>
<string name="debug_notification_channel_label">Notifiche di Debug</string>
<string name="debug_debuglog_file_label">Log file salvato</string>
<string name="debug_debuglog_file_label">File di log registrati</string>
<string name="debug_debuglog_record_action">Log di debug</string>
<string name="settings_support_installid_label">ID installazione</string>
<string name="settings_support_installid_desc">Le segnalazioni automatiche degli errori sono anonime. Condividi il tuo ID di installazione con lo sviluppatore se necessità di trovare la tua segnalazione all\'interno del registro segnalazioni.</string>
<string name="settings_support_installid_label">ID d\'installazione</string>
<string name="settings_support_installid_desc">Le segnalazioni automatiche degli errori sono anonime. Condividi il tuo ID d\'installazione con lo sviluppatore se necessita di trovare la tua segnalazione all\'interno del registro segnalazioni.</string>
<string name="settings_support_label">Supporto</string>
<string name="settings_support_description">Se ti serve un aiuto.</string>
<string name="issue_tracker_label">Tracker problemi</string>
<string name="issue_tracker_description">Un issue tracker pubblico per segnalare i problemi e le richieste per aggiungere nuove funzionalità (solo in inglese).</string>
<string name="settings_support_description">Se ti serve aiuto.</string>
<string name="issue_tracker_label">Tracker di problemi</string>
<string name="issue_tracker_description">Un tracker di problemi pubblico per segnalare problemi e richieste di nuove funzionalità (solo in inglese).</string>
<string name="discord_label">Discord</string>
<string name="discord_description">Un posto dove poter fare domande.</string>
<string name="changelog_label">Changelog</string>
@@ -77,11 +83,27 @@
<string name="help_translate_label">Traduzione</string>
<string name="help_translate_description">Aiuta a tradurre l\'applicazione nella tua lingua preferita.</string>
<string name="translators_thanks_title">Traduttori</string>
<string name="translators_thanks_description">darken</string>
<string name="translators_thanks_description">Filippo Lolli (IG: @lolli_filippo)</string>
<string name="widget_description">Un widget che mostra l\'ultimo stato del dispositivo.</string>
<string name="settings_compat_indirectcallback_title">Consegna indiretta dei dati</string>
<string name="settings_compat_indirectcallback_summary">Utilizza un metodo alternativo per ricevere dati BLE dal sistema (broadcast invece di callback).</string>
<string name="troubleshooter_title">Risoluzione dei problemi</string>
<string name="troubleshooter_ble_intro_title">Trasmissioni con Bluetooth Low Energy</string>
<string name="troubleshooter_ble_intro_body1">Le AirPods (e cuffie simili) trasmettono le informazioni di stato utilizzando una tecnologia BLE chiamata \"annunci\". Alcuni smartphone non implementano questa tecnologia correttamente. CAPod può provare a risolvere ciò provando opzioni compatibili differenti fino a quando i dati vengono ricevuti. Riproduci della musica nelle cuffie e mettile vicino al tuo smartphone, successivamente avvia la procedura.</string>
<string name="troubleshooter_ble_intro_start_action">Avvia la risoluzione dei problemi</string>
<string name="troubleshooter_ble_process_title">Risoluzione dei problemi in corso</string>
<string name="troubleshooter_ble_process_subtile">Ogni passaggio richiede 5-10 secondi</string>
<string name="troubleshooter_ble_result_success_title">Eseguito</string>
<string name="troubleshooter_ble_result_success_body">La trasmissione \"annunci\" BLE è stata ricevuta da CAPod.</string>
<string name="troubleshooter_ble_result_failure_title">Fallito</string>
<string name="troubleshooter_ble_result_failure_body">Diagnostica fallita. Nessuna combinazione di opzioni compatibili ha funzionato.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Il tuo smartphone non ha ricevuto dati BLE. Puoi riprovare questo test in un area affollata per vedere se le fonti dati (diverse dalle tue cuffie) vengono ricevuti. La mancata ricezione indica un possibile problema del sistema operativo del tuo smartphone.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Il tuo smartphone ha ricevuto i dati BLE, ma i dati non provengono da un dispositivo supportato. Le tue cuffie sono accese/cariche? CAPod supporta le tue cuffie?</string>
<string name="troubleshooter_ble_result_failure_action">Riprova</string>
<string name="troubleshoot_action">Risoluzione dei problemi</string>
<string name="onboarding_body1">Questa applicazione fornisce supporto per le funzionalità specifiche dell AirPods su Android.</string>
<string name="onboarding_body2">Non tutti i dispositivi Android supportano le funzionalità extra delle AirPods. Un prerequisito del tuo sistema operativo è l\'implementazione di Bluetooth-Low-Energy.</string>
<string name="onboarding_body3">CAPod non contiene pubblicità e non raccoglie i tuoi dati.</string>
<string name="onboarding_body4">Puoi aggiornare alla versione Pro per sbloccare funzioni aggiuntive e supportare lo sviluppo.</string>
<string name="general_continue_action">Continua</string>
</resources>
+9
View File
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">Model peranti utama anda. Ini membantu apl mengenali peranti anda apabila ia tidak disambungkan ke telefon anda.</string>
<string name="settings_onepod_mode_label">Satu mod pod</string>
<string name="settings_onepod_mode_description">Tidak perlu memakai kedua-dua pod, memakai satu pod sudah memadai untuk mencetuskan tindak balas.</string>
<string name="settings_popup_caseopen_label">Tunjukkan pop timbul kes</string>
<string name="settings_popup_caseopen_description">Tunjukkan popup apabila sarung peranti dibuka (percubaan).</string>
<string name="settings_popup_connected_label">Tunjukkan pop timbul sambungan</string>
<string name="settings_popup_connected_description">Tunjukkan pop timbul apabila peranti terhubung buat kali pertama.</string>
<string name="notification_channel_device_status_label">Status peranti</string>
<string name="debug_debuglog_size_label">Saiz</string>
<string name="debug_debuglog_size_compressed_label">Saiz termampat</string>
@@ -94,7 +97,13 @@
<string name="troubleshooter_ble_result_success_body">Siaran iklan BLE sedang diterima oleh CAPod.</string>
<string name="troubleshooter_ble_result_failure_title">Tidak berjaya</string>
<string name="troubleshooter_ble_result_failure_body">Penyelesaian masalah gagal. Tiada gabungan pilihan keserasian dapat membantu.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Telefon anda tidak menerima sebarang data BLE sama sekali.. Anda boleh mencuba semula ujian ini di kawasan yang sesak untuk melihat sama ada sumber data (selain fon kepala anda) boleh diterima. Tiada data yang diterima menunjukkan masalah dengan sistem pengendalian telefon anda.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Telefon anda menerima data BLE, tetapi data itu tidak datang daripada mana-mana peranti yang disokong. Adakah fon kepala anda dihidupkan? Adakah CAPod menyokong fon kepala anda?</string>
<string name="troubleshooter_ble_result_failure_action">Cuba semula</string>
<string name="troubleshoot_action">Selesaikan masalah</string>
<string name="onboarding_body1">Aplikasi ini menambah sokongan untuk ciri khusus AirPod pada Android.</string>
<string name="onboarding_body2">Tidak semua peranti Android menyokong sepenuhnya ciri AirPod tambahan. Sistem pengendalian anda memerlukan pelaksanaan Bluetooth-Tenaga-Rendah yang berfungsi dengan betul.</string>
<string name="onboarding_body3">CAPod tiada iklan serta tidak mengumpul data anda.</string>
<string name="onboarding_body4">Anda boleh menaik taraf kepada CAPod Pro untuk mendapatkan ciri tambahan serta pembangunan sokongan.</string>
<string name="general_continue_action">Teruskan</string>
</resources>
+1
View File
@@ -28,6 +28,7 @@
<item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
<item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item tools:targetApi="29" name="android:enforceNavigationBarContrast">true</item>
<item tools:targetApi="29" name="android:navigationBarColor">@android:color/transparent</item>
</style>
+9
View File
@@ -45,7 +45,10 @@
<string name="settings_maindevice_model_description">Het model van uw hoofdapparaat. Dit helpt de app uw apparaat te herkennen wanneer het niet is verbonden met uw telefoon.</string>
<string name="settings_onepod_mode_label">Eén pod-modus</string>
<string name="settings_onepod_mode_description">Het dragen van beide pods is niet vereist, het dragen van een enkele pod is voldoende om reacties uit te lokken.</string>
<string name="settings_popup_caseopen_label">Case-pop-up weergeven</string>
<string name="settings_popup_caseopen_description">Een pop-up weergeven wanneer de behuizing van het apparaat wordt geopend (experimenteel).</string>
<string name="settings_popup_connected_label">Verbindingspop-up weergeven</string>
<string name="settings_popup_connected_description">Toon een pop-up wanneer het apparaat voor de eerste keer verbinding maakt.</string>
<string name="notification_channel_device_status_label">Apparaatstatus</string>
<string name="debug_debuglog_size_label">Grootte</string>
<string name="debug_debuglog_size_compressed_label">Gecomprimeerde grootte</string>
@@ -94,7 +97,13 @@
<string name="troubleshooter_ble_result_success_body">BLE-advertentie-uitzendingen worden door CAPod ontvangen.</string>
<string name="troubleshooter_ble_result_failure_title">Mislukt</string>
<string name="troubleshooter_ble_result_failure_body">Problemen oplossen mislukt. Geen enkele combinatie van compatibiliteitsopties heeft geholpen.</string>
<string name="troubleshooter_ble_result_failure_phone_body">Je telefoon heeft helemaal geen BLE-gegevens ontvangen. Je kunt deze test opnieuw proberen in een druk gebied om te zien of gegevensbronnen (anders dan uw hoofdtelefoon) kunnen worden ontvangen. Er worden geen gegevens ontvangen die wijzen op een probleem met het besturingssysteem van uw telefoon.</string>
<string name="troubleshooter_ble_result_failure_phone_headphones">Je telefoon heeft BLE-gegevens ontvangen, maar de gegevens zijn niet afkomstig van een ondersteund apparaat. Staat je koptelefoon aan? Ondersteunt CAPod je koptelefoon?</string>
<string name="troubleshooter_ble_result_failure_action">Probeer opnieuw</string>
<string name="troubleshoot_action">Problemen oplossen</string>
<string name="onboarding_body1">Deze app voegt ondersteuning voor AirPod-specifieke functies toe aan Android.</string>
<string name="onboarding_body2">Niet alle Android-apparaten ondersteunen de extra AirPod-functies volledig. Je besturingssysteem vereist een correct werkende Bluetooth-Low-Energy implementatie.</string>
<string name="onboarding_body3">CAPod heeft geen advertenties en verzamelt je gegevens niet.</string>
<string name="onboarding_body4">Je kunt upgraden naar CAPod Pro om extra functies te krijgen en de ontwikkeling te ondersteunen.</string>
<string name="general_continue_action">Doorgaan</string>
</resources>

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